Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't schedule proxy builds to inproc node if their configs previously built on oop nodes #6635

Merged
merged 3 commits into from
Jul 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions src/Build.UnitTests/ProjectCache/ProjectCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,30 @@ public enum ErrorKind
LoggedError
}

public class ConfigurableMockCache : ProjectCachePluginBase
{
public Func<BuildRequestData, PluginLoggerBase, CancellationToken, Task<CacheResult>>? GetCacheResultImplementation { get; set; }
public override Task BeginBuildAsync(CacheContext context, PluginLoggerBase logger, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}

public override Task<CacheResult> GetCacheResultAsync(
BuildRequestData buildRequest,
PluginLoggerBase logger,
CancellationToken cancellationToken)
{
return GetCacheResultImplementation != null
? GetCacheResultImplementation(buildRequest, logger, cancellationToken)
: Task.FromResult(CacheResult.IndicateNonCacheHit(CacheResultType.CacheNotApplicable));
}

public override Task EndBuildAsync(PluginLoggerBase logger, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}

public class InstanceMockCache : ProjectCachePluginBase
{
private readonly GraphCacheResponse? _testData;
Expand Down Expand Up @@ -1475,6 +1499,97 @@ public void ParallelStressTest(bool useSynchronousLogging, bool disableInprocNod
cache.QueryStartStops.Count.ShouldBe(graph.ProjectNodes.Count * 2);
}

[Fact]
// Schedules different requests for the same BuildRequestConfiguration in parallel.
// The first batch of the requests are cache misses, the second batch are cache hits via proxy builds.
// The first batch is delayed so it starts intermingling with the second batch.
// This test ensures that scheduling proxy builds on the inproc node works nicely within the Scheduler
// if the BuildRequestConfigurations for those proxy builds have built before (or are still building) on
// the out of proc node.
// More details: https://github.com/dotnet/msbuild/pull/6635
public void ProxyCacheHitsOnPreviousCacheMissesShouldWork()
cdmihai marked this conversation as resolved.
Show resolved Hide resolved
{
var cacheNotApplicableTarget = "NATarget";
var cacheHitTarget = "CacheHitTarget";
var proxyTarget = "ProxyTarget";

var project =
@$"
<Project>
<Target Name='{cacheNotApplicableTarget}'>
<Exec Command=`{Helpers.GetSleepCommand(TimeSpan.FromMilliseconds(200))}` />
<Message Text='{cacheNotApplicableTarget} in $(MSBuildThisFile)' />
</Target>

<Target Name='{cacheHitTarget}'>
<Message Text='{cacheHitTarget} in $(MSBuildThisFile)' />
</Target>

<Target Name='{proxyTarget}'>
<Message Text='{proxyTarget} in $(MSBuildThisFile)' />
</Target>
</Project>
".Cleanup();

var projectPaths = Enumerable.Range(0, NativeMethodsShared.GetLogicalCoreCount())
.Select(i => _env.CreateFile($"project{i}.proj", project).Path)
.ToArray();

var cacheHitCount = 0;
var nonCacheHitCount = 0;

var buildParameters = new BuildParameters
{
ProjectCacheDescriptor = ProjectCacheDescriptor.FromInstance(
new ConfigurableMockCache
{
GetCacheResultImplementation = (request, _, _) =>
{
var projectFile = request.ProjectFullPath;

if (request.TargetNames.Contains(cacheNotApplicableTarget))
{
Interlocked.Increment(ref nonCacheHitCount);
return Task.FromResult(CacheResult.IndicateNonCacheHit(CacheResultType.CacheNotApplicable));
}
else
{
Interlocked.Increment(ref cacheHitCount);
return Task.FromResult(
CacheResult.IndicateCacheHit(
new ProxyTargets(new Dictionary<string, string> {{proxyTarget, cacheHitTarget}})));
}
}
},
projectPaths.Select(p => new ProjectGraphEntryPoint(p)).ToArray(),
projectGraph: null),
MaxNodeCount = NativeMethodsShared.GetLogicalCoreCount()
};

using var buildSession = new Helpers.BuildManagerSession(_env, buildParameters);

var buildRequests = new List<(string, string)>();
buildRequests.AddRange(projectPaths.Select(r => (r, cacheNotApplicableTarget)));
buildRequests.AddRange(projectPaths.Select(r => (r, cacheHitTarget)));

var buildTasks = new List<Task<BuildResult>>();
foreach (var (projectPath, target) in buildRequests)
{
buildTasks.Add(buildSession.BuildProjectFileAsync(projectPath, new[] {target}));
}

foreach (var buildResult in buildTasks.Select(buildTask => buildTask.Result))
{
buildResult.Exception.ShouldBeNull();
buildResult.OverallResult.ShouldBe(BuildResultCode.Success);
}

buildSession.Logger.ProjectStartedEvents.Count.ShouldBe(2 * projectPaths.Length);

cacheHitCount.ShouldBe(projectPaths.Length);
nonCacheHitCount.ShouldBe(projectPaths.Length);
}

private static void StringShouldContainSubstring(string aString, string substring, int expectedOccurrences)
{
aString.ShouldContain(substring);
Expand Down
13 changes: 13 additions & 0 deletions src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
Expand Down Expand Up @@ -102,6 +103,9 @@ private async Task BeginBuildAsync(ProjectCacheDescriptor? vsWorkaroundOverrideD
{
SetState(ProjectCacheServiceState.BeginBuildStarted);

logger.LogMessage("Initializing project cache plugin", MessageImportance.Low);
var timer = Stopwatch.StartNew();

if (_projectCacheDescriptor.VsWorkaround)
{
logger.LogMessage("Running project cache with Visual Studio workaround");
Expand All @@ -118,6 +122,9 @@ await _projectCachePlugin.BeginBuildAsync(
logger,
_cancellationToken);

timer.Stop();
logger.LogMessage($"Finished initializing project cache plugin in {timer.Elapsed.TotalMilliseconds} ms", MessageImportance.Low);

SetState(ProjectCacheServiceState.BeginBuildFinished);
}
catch (Exception e)
Expand Down Expand Up @@ -413,8 +420,14 @@ public async Task ShutDown()
{
SetState(ProjectCacheServiceState.ShutdownStarted);

logger.LogMessage("Shutting down project cache plugin", MessageImportance.Low);
var timer = Stopwatch.StartNew();

await _projectCachePlugin.EndBuildAsync(logger, _cancellationToken);

timer.Stop();
logger.LogMessage($"Finished shutting down project cache plugin in {timer.Elapsed.TotalMilliseconds} ms", MessageImportance.Low);
cdmihai marked this conversation as resolved.
Show resolved Hide resolved

if (logger.HasLoggedErrors)
{
ProjectCacheException.ThrowForErrorLoggedInsideTheProjectCache("ProjectCacheShutdownFailed");
Expand Down
8 changes: 6 additions & 2 deletions src/Build/BackEnd/Components/Scheduler/Scheduler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1385,7 +1385,7 @@ private void AssignUnscheduledRequestToNode(SchedulableRequest request, int node

void WarnWhenProxyBuildsGetScheduledOnOutOfProcNode()
{
if (request.IsProxyBuildRequest() && nodeId != InProcNodeId)
if (request.IsProxyBuildRequest() && nodeId != InProcNodeId && _schedulingData.CanScheduleRequestToNode(request, InProcNodeId))
{
ErrorUtilities.VerifyThrow(
_componentHost.BuildParameters.DisableInProcNode || _forceAffinityOutOfProc,
Expand Down Expand Up @@ -2107,7 +2107,11 @@ private NodeAffinity GetNodeAffinityForRequest(BuildRequest request)
return NodeAffinity.InProc;
}

if (request.IsProxyBuildRequest())
ErrorUtilities.VerifyThrow(request.ConfigurationId != BuildRequestConfiguration.InvalidConfigurationId, "Requests should have a valid configuration id at this point");
// If this configuration has been previously built on an out of proc node, scheduling it on the inproc node can cause either an affinity mismatch error when
// there are other pending requests for the same configuration or "unscheduled requests remain in the presence of free out of proc nodes" errors if there's no pending requests.
// So only assign proxy builds to the inproc node if their config hasn't been previously assigned to an out of proc node.
if (_schedulingData.CanScheduleConfigurationToNode(request.ConfigurationId, InProcNodeId) && request.IsProxyBuildRequest())
{
return NodeAffinity.InProc;
}
Expand Down
7 changes: 6 additions & 1 deletion src/Build/BackEnd/Components/Scheduler/SchedulingData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,12 @@ public int GetAssignedNodeForRequestConfiguration(int configurationId)
/// </summary>
public bool CanScheduleRequestToNode(SchedulableRequest request, int nodeId)
{
int requiredNodeId = GetAssignedNodeForRequestConfiguration(request.BuildRequest.ConfigurationId);
return CanScheduleConfigurationToNode(request.BuildRequest.ConfigurationId, nodeId);
}

public bool CanScheduleConfigurationToNode(int configurationId, int nodeId)
{
int requiredNodeId = GetAssignedNodeForRequestConfiguration(configurationId);
return requiredNodeId == Scheduler.InvalidNodeId || requiredNodeId == nodeId;
}

Expand Down