Skip to content

Replace ProjectCacheService null Project crash with diagnostic telemetry#13396

Merged
YuliiaKovalova merged 1 commit into
mainfrom
dev/fix-projectcache-null-project
Mar 17, 2026
Merged

Replace ProjectCacheService null Project crash with diagnostic telemetry#13396
YuliiaKovalova merged 1 commit into
mainfrom
dev/fix-projectcache-null-project

Conversation

@YuliiaKovalova

@YuliiaKovalova YuliiaKovalova commented Mar 17, 2026

Copy link
Copy Markdown
Member

Summary

HandleBuildResultAsync in ProjectCacheService crashes with InternalErrorException("Project unexpectedly null") when a build result arrives for a configuration whose ProjectInstance was never loaded on the in-proc node.

Root Cause

In VS (and global plugin) scenarios, ShouldUseCache returns true without checking IsLoaded. This is by design — it's needed for the pre-build cache request path (BuildManager.ExecuteSubmission), where the project hasn't been evaluated yet and PostCacheRequest loads it via EvaluateProjectIfNecessary.

However, ShouldUseCache is also called from the post-build path (BuildManager.HandleResult) to decide whether to notify cache plugins about the result. When a project was built on an out-of-proc worker node, the in-proc config cache entry never gets a ProjectInstance assigned — ProjectInstance is intentionally not transferred back from worker nodes because it's too large to serialize across the named pipe (only BuildResult with target outcomes is returned).

This means HandleBuildResultAsync is called with a configuration where IsLoaded is false and accessing .Project would crash.

Fix

Replace the VerifyThrowInternalNull(requestConfiguration.Project) assert with an !requestConfiguration.IsLoaded early return.

Why IsLoaded instead of Project == null: The Project getter has an internal assert (!IsCached) that throws if the configuration happens to be in cached-to-disk state. IsLoaded is a safe read-only check (_project?.IsLoaded == true) with no side effects or asserts. It also correctly handles the edge case of a partially-initialized ProjectInstance (deserialized but not yet LateInitialize'd).

Skipping is safe because:

  • The build has already completed successfully at this point
  • The cache plugin's HandleProjectFinishedAsync requires a loaded ProjectInstance to determine applicable plugins and provide project context
  • No build correctness is affected — only the cache plugin notification is skipped for this configuration

Validation

Diagnostic telemetry (ProjectCacheState) deployed in a prior iteration confirmed the root cause across multiple VS repo users:

  • IsLoaded=False, IsCached=False, WasGeneratedByNode=False, IsVsScenario=True
  • Configuration created from BuildRequestData without ProjectInstance (VS submits by path)
  • Project built on out-of-proc worker node, ProjectInstance never transferred back

Copilot AI review requested due to automatic review settings March 17, 2026 09:46
@YuliiaKovalova YuliiaKovalova marked this pull request as draft March 17, 2026 09:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Prevents MSBuild builds from crashing when ProjectCacheService.HandleBuildResultAsync encounters a BuildRequestConfiguration whose ProjectInstance was never loaded locally (notably possible in VS scenarios). Instead of asserting, it logs diagnostic telemetry and safely skips cache result handling for that configuration.

Changes:

  • Replace VerifyThrowInternalNull(requestConfiguration.Project) with a null check.
  • Emit telemetry event vs/msbuild/projectcache/nullproject capturing relevant configuration state.
  • Return early (skip plugin result handling) when requestConfiguration.Project is null.

new Dictionary<string, string>
{
["ConfigurationId"] = requestConfiguration.ConfigurationId.ToString(),
["ProjectFullPath"] = Path.GetFileName(requestConfiguration.ProjectFullPath),
Comment on lines +828 to +830
{
["ConfigurationId"] = requestConfiguration.ConfigurationId.ToString(),
["ProjectFullPath"] = Path.GetFileName(requestConfiguration.ProjectFullPath),
Comment on lines +819 to +838
// The Project property may be null if the configuration was created from a remote node
// request or the project was never loaded locally (e.g., in VS scenario where ShouldUseCache
// returns true before checking IsLoaded). Skip cache result handling in this case.
if (requestConfiguration.Project is null)
{
_loggingService.LogTelemetry(
buildEventContext,
"vs/msbuild/projectcache/nullproject",
new Dictionary<string, string>
{
["ConfigurationId"] = requestConfiguration.ConfigurationId.ToString(),
["ProjectFullPath"] = Path.GetFileName(requestConfiguration.ProjectFullPath),
["IsLoaded"] = requestConfiguration.IsLoaded.ToString(),
["IsCached"] = requestConfiguration.IsCached.ToString(),
["WasGeneratedByNode"] = requestConfiguration.WasGeneratedByNode.ToString(),
["IsVsScenario"] = _isVsScenario.ToString(),
["HasGlobalPlugins"] = (_globalProjectCacheDescriptor is not null).ToString(),
});
return;
}
Comment on lines +828 to +835
{
["ConfigurationId"] = requestConfiguration.ConfigurationId.ToString(),
["ProjectFullPath"] = Path.GetFileName(requestConfiguration.ProjectFullPath),
["IsLoaded"] = requestConfiguration.IsLoaded.ToString(),
["IsCached"] = requestConfiguration.IsCached.ToString(),
["WasGeneratedByNode"] = requestConfiguration.WasGeneratedByNode.ToString(),
["IsVsScenario"] = _isVsScenario.ToString(),
["HasGlobalPlugins"] = (_globalProjectCacheDescriptor is not null).ToString(),
…ceful skip

HandleBuildResultAsync crashed with InternalErrorException when
configuration.Project was null. This can happen in the VS scenario
where ShouldUseCache returns true before checking IsLoaded, allowing
a configuration without a loaded ProjectInstance to reach this code.

Instead of crashing, emit diagnostic telemetry with the configuration
state (ConfigurationId, IsLoaded, IsCached, WasGeneratedByNode,
IsVsScenario) and gracefully skip cache result handling.
@YuliiaKovalova YuliiaKovalova force-pushed the dev/fix-projectcache-null-project branch from 356d931 to 328236d Compare March 17, 2026 09:50
@YuliiaKovalova YuliiaKovalova merged commit 328236d into main Mar 17, 2026
1 of 9 checks passed
@YuliiaKovalova YuliiaKovalova deleted the dev/fix-projectcache-null-project branch March 17, 2026 09:57
This was referenced May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants