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

Introduce cross-process resource management for tasks #5859

Merged
merged 118 commits into from Mar 30, 2021
Merged
Show file tree
Hide file tree
Changes from 112 commits
Commits
Show all changes
118 commits
Select commit Hold shift + click to select a range
f48f3d9
Initial draft of changes.
benvillalobos Feb 25, 2020
bcccfea
Modified mockengine to support ibuildengine7
benvillalobos Feb 26, 2020
73b3466
Simpler null checks
benvillalobos Feb 28, 2020
a8df6ac
Merge remote-tracking branch 'upstream/master' into 74-cpp-parallel
rainersigwald Mar 4, 2020
963a785
Clean up API surface
rainersigwald Mar 4, 2020
e1a30e6
Tasks type name
rainersigwald Mar 5, 2020
055116a
simple project
rainersigwald Mar 5, 2020
10f6eed
Simplify if-appdomain in NodeConfiguration
rainersigwald Mar 5, 2020
aa3f385
Checkpoint
rainersigwald Mar 6, 2020
57a5c93
Remove FEATURE_VARIOUS_EXCEPTIONS
rainersigwald Mar 6, 2020
9229122
Checkpoint: works on full only, doesn't properly block when all resou…
rainersigwald Mar 9, 2020
b919834
Make TaskHost.MarkAsInactive work on Core
rainersigwald Mar 9, 2020
01953aa
Tweak test to show cross-process handling
rainersigwald Mar 12, 2020
1183e87
Introduce RequireCores
rainersigwald Mar 12, 2020
0437c32
WIP
rainersigwald Mar 12, 2020
1bf3017
Revert "WIP"
rainersigwald Mar 12, 2020
1ada7c2
Horrible pile of WIP hacks to debug hang
rainersigwald Apr 20, 2020
e943655
Revert "Horrible pile of WIP hacks to debug hang"
rainersigwald Apr 20, 2020
ca6697d
Move requiring core to ExecuteInstantiatedTask
rainersigwald Apr 20, 2020
63a324b
Merge remote-tracking branch 'upstream/master' into exp/resource-mana…
rainersigwald Apr 27, 2020
09618ec
Release core when yielding (hopefully working around hang when many t…
rainersigwald May 1, 2020
5a7ad2b
WIP: new semaphore name per session (by default)
rainersigwald Jun 10, 2020
141c57c
Merge remote-tracking branch 'upstream/master' into exp/resource-mana…
rainersigwald Jun 11, 2020
007d9f3
Release a core when calling BuildProjectFiles
rainersigwald Jun 16, 2020
6b2be21
Switch expression for MockHost.GetComponent
rainersigwald Jun 16, 2020
5d49876
Resource manager in MockHost
rainersigwald Jun 16, 2020
0486730
Just don't do resource management on non-Windows
rainersigwald Jun 16, 2020
2f85924
Delete bogus tests
rainersigwald Jun 16, 2020
5e4b035
Doc for RequestCores
rainersigwald Jun 17, 2020
a9d37a2
Merge remote-tracking branch 'upstream/master' into exp/resource-mana…
rainersigwald Jun 17, 2020
43bdec0
Merge remote-tracking branch 'upstream/master' into exp/resource-mana…
rainersigwald Jul 15, 2020
479cdfc
Add BlockingWaitForCore
rainersigwald Jul 15, 2020
9db29bb
Treat resources as a separate pool; don't auto-acquire for tasks
rainersigwald Jul 15, 2020
e1c3bff
fixup! Add BlockingWaitForCore
rainersigwald Jul 15, 2020
58fa355
Doc updates
rainersigwald Jul 15, 2020
a6a9d6d
Merge remote-tracking branch 'upstream/master' into exp/resource-mana…
rainersigwald Sep 16, 2020
91fe07a
Block for at least one core in RequestCores
rainersigwald Sep 16, 2020
c9b3c78
Merge remote-tracking branch 'upstream/master' into exp/resource-mana…
rainersigwald Oct 21, 2020
ba2db9a
Remove BlockingWaitForCore() since it's now redundant
rainersigwald Oct 21, 2020
cc60df9
fixup! Block for at least one core in RequestCores
rainersigwald Oct 21, 2020
0ab0481
Move to IBuildEngine8 since 7 shipped already
rainersigwald Oct 21, 2020
f1e26c3
remove test-project.proj
rainersigwald Nov 3, 2020
dfced2f
Remove SemaphoreCPUTask
rainersigwald Nov 3, 2020
ed6a404
??
rainersigwald Nov 3, 2020
4545858
sort usings in MockHost
rainersigwald Nov 5, 2020
3de11c0
fixup! Treat resources as a separate pool; don't auto-acquire for tasks
rainersigwald Nov 5, 2020
608bed7
fixup! Remove BlockingWaitForCore() since it's now redundant
rainersigwald Nov 5, 2020
7a3da6b
fixup! Treat resources as a separate pool; don't auto-acquire for tasks
rainersigwald Nov 5, 2020
d87903f
Remove RequireCores
rainersigwald Nov 5, 2020
9a83f35
Better non-Windows behavior
rainersigwald Nov 5, 2020
77344e2
fixup! Move to IBuildEngine8 since 7 shipped already
rainersigwald Nov 5, 2020
7fffd46
Generalize MockEngine semaphore
rainersigwald Nov 5, 2020
7d8b359
Return nullable int to indicate whether resource management is possible
rainersigwald Dec 7, 2020
82a25c4
fixup! Remove RequireCores
rainersigwald Dec 7, 2020
c6d1a8a
Merge remote-tracking branch 'upstream/master' into HEAD
rainersigwald Dec 8, 2020
b8b52cc
Nix whitespace-only changes in ProjectCollection
rainersigwald Nov 20, 2020
f3e347e
fixup! fixup! Remove BlockingWaitForCore() since it's now redundant
rainersigwald Nov 20, 2020
ab384aa
Task whitespace fixes
rainersigwald Nov 20, 2020
a9d8061
Update doc for non-Windows behavior
rainersigwald Nov 20, 2020
25ec10c
Update doc with better example, caveats
rainersigwald Nov 20, 2020
b362600
Whitespace fixes in TaskBuilder
rainersigwald Nov 20, 2020
b830c49
Whitespace cleanup in TaskHost
rainersigwald Nov 20, 2020
bb2d947
Clarity in MockHost
rainersigwald Nov 20, 2020
59af842
fixup! fixup! fixup! Remove BlockingWaitForCore() since it's now redu…
rainersigwald Nov 20, 2020
cacc2ac
Log resource requests/releases
rainersigwald Nov 23, 2020
3c0a8e7
Move clamp on release to service layer
rainersigwald Dec 8, 2020
ded86d5
Switch system off more gracefully on non-Windows
rainersigwald Dec 8, 2020
ad77314
Merge remote-tracking branch 'upstream/master' into exp/resource-mana…
rainersigwald Dec 14, 2020
0c0c0b4
Release nodes on reacquire
rainersigwald Dec 15, 2020
a16ca54
Merge remote-tracking branch 'upstream/master' into exp/resource-mana…
rainersigwald Dec 15, 2020
1a497d7
Merge remote-tracking branch 'upstream/master' into exp/resource-mana…
rainersigwald Dec 15, 2020
7a4e1e0
Implicit core for nonblocking 1 return
rainersigwald Jan 5, 2021
3d2575c
Allow environment variable MSBUILDRESOURCEMANAGEROVERSUBSCRIPTION to …
rainersigwald Jan 5, 2021
c416bf5
Revert "Release nodes on reacquire"
rainersigwald Jan 5, 2021
444ffde
WIP: acquire/release resources in the scheduler
rainersigwald Jan 15, 2021
761fb6a
Update documentation/specs/resource-management.md
cdmihai Jan 15, 2021
2fb5789
Merge remote-tracking branch 'dotnet/master' into exp/resource-manage…
ladipro Feb 17, 2021
2ce2af8
Remove Semaphore-based logic
ladipro Feb 19, 2021
e3439e4
Plumbing to pass ResourceRequest to scheduler and ResourceResponse ba…
ladipro Feb 22, 2021
1653806
Add missing files: ResourceRequest.cs, ResourceResponse.cs
ladipro Feb 22, 2021
6d27a28
Plumbing fixes
ladipro Feb 24, 2021
df40170
Implement scheduling policy
ladipro Feb 24, 2021
3d03f86
Use 'implicit core' always, not only when scheduler returns 0
ladipro Mar 1, 2021
ba264da
Use different limits for scheduling and explicit core requests
ladipro Mar 1, 2021
d12bd75
Make RequestCores ignore the executing request count
ladipro Mar 5, 2021
1a6326e
Subtract one from _coreLimit
ladipro Mar 5, 2021
0b1a487
Change return value of RequestCores to int (null is no longer used)
ladipro Mar 12, 2021
d38500b
Do not assume that RequestCores calls come only from Executing requests
ladipro Mar 12, 2021
445ec33
Make RequestCores block and wait for at least one core
ladipro Mar 16, 2021
a98adee
Revert "Use 'implicit core' always, not only when scheduler returns 0"
ladipro Mar 16, 2021
15121be
Make the first RequestCores call non-blocking (via 'implicit' core)
ladipro Mar 16, 2021
02bce34
Make the implicit core the last one to release (LIFO)
ladipro Mar 16, 2021
b382aab
Introduce MSBUILDNODECOREALLOCATIONWEIGHT
ladipro Mar 16, 2021
8dbe055
Merge remote-tracking branch 'dotnet/main' into exp/resource-management
ladipro Mar 19, 2021
14c5d3d
Allow calling RequestCores/ReleaseCores after Yield
ladipro Mar 23, 2021
aad6f76
Update resource-management.md
ladipro Mar 24, 2021
3a554b8
Comments, renames, and tweaks
ladipro Mar 24, 2021
63d782e
Remove the now unused ResourceManagerService
ladipro Mar 24, 2021
874ecf3
Comments, renames, and tweaks
ladipro Mar 25, 2021
8ed06ad
Comments, renames, and tweaks
ladipro Mar 25, 2021
97e9f5d
Revert string changes
ladipro Mar 25, 2021
531ee86
Tweaks in Scheduler.cs
ladipro Mar 25, 2021
27865a8
Refactor SchedulingData & SchedulableRequest, don't consider nodes wi…
ladipro Mar 25, 2021
8016cf9
Don't wait for ResourceResponse under a lock
ladipro Mar 26, 2021
d10923f
Release all cores on reacquire
ladipro Mar 26, 2021
3ac6642
Renames and tweaks
ladipro Mar 26, 2021
059ac7b
Add unit tests to TaskHost_Tests
ladipro Mar 26, 2021
1340f7c
Fix bug in scheduler where cores were not granted during build rundown
ladipro Mar 26, 2021
f2381ad
Rework RequestCores concurrency
ladipro Mar 26, 2021
605f6f2
Fix ArgumentOutOfRangeException in ReleaseAllCores
ladipro Mar 26, 2021
876fef6
Add ResourceManagement_Tests
ladipro Mar 26, 2021
f9f0185
Move state check and make tests Core-compatible
ladipro Mar 26, 2021
719425b
PR feedback: Make instantiation of ResourceRequest more readable
ladipro Mar 29, 2021
2a6cabe
PR feedback: Move the new API to IBuildEngine9
ladipro Mar 29, 2021
8b53959
PR feedback: Add comments to RequestBuilder.RequestCores
ladipro Mar 29, 2021
74046a0
PR feedback: Document callers of IRequestBuilderCallback.RequestCores…
ladipro Mar 29, 2021
93db0c1
PR feedback: Comment new environment variables
ladipro Mar 30, 2021
de74af6
PR feedback: Add RequestCores/ReleaseCores logging
ladipro Mar 30, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
41 changes: 41 additions & 0 deletions documentation/specs/resource-management.md
@@ -0,0 +1,41 @@
# Managing tools with their own parallelism in MSBuild

MSBuild supports building projects in parallel using multiple processes. Most users opt into `Environment.ProcessorCount` parallelism at the MSBuild layer.

In addition, tools sometimes support parallel execution. The Visual C++ compiler `cl.exe` supports `/MP[n]`, which parallelizes compilation at the translation-unit (file) level. If a number isn't specified, it defaults to `NUM_PROCS`.
Copy link
Member

Choose a reason for hiding this comment

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

This section is well-written, but it reads to me as general statement of purpose --> example --> actually, we only care about the example. Although cl.exe was the motivating example, I imagine others will start using it later, so I'd focus on describing this generally rather than diving into a specific example in the first section. On a related note, are you planning to make documentation not in the specs folder? If so, this may be a moot point.


When used in combination, `NUM_PROCS * NUM_PROCS` compiler processes can be launched, all of which would like to do file I/O and intense computation. This generally overwhelms the operating system's scheduler and causes thrashing and terrible build times.

As a result, the standard guidance is to use only one multiproc option: MSBuild's _or_ `cl.exe`'s. But that leaves the machine underloaded when things could be happening in parallel.

## Design

`IBuildEngine` will be extended to allow a task to indicate to MSBuild that it would like to consume more than one CPU core (`RequestCores`). These will be advisory only — a task can still do as much work as it desires with as many threads and processes as it desires.

A cooperating task would limit its own parallelism to the number of CPU cores MSBuild can reserve for the requesting task.

`RequestCores(int requestedCores)` will always return a positive value, possibly less than the parameter if that many cores are not available. If no cores are available at the moment, the call blocks until at least one becomes available. The first `RequestCores` call made by a task is guaranteed to be non-blocking, though, as at minimum it will return the "implicit" core allocated to the task itself. This leads to two conceptual ways of adopting the API. Either the task calls `RequestCores` once, passing the desired number of cores, and then limiting its parallelism to whatever the call returns. Or the task makes additional calls throughout its execution, perhaps as it discovers more work to do. In this second scenario the task must be OK with waiting for additional cores for a long time or even forever if the sum of allocated cores has exceeded the limit defined by the policy.

All resources acquired by a task will be automatically returned when the task's `Execute()` method returns, and a task can optionally return a subset by calling `ReleaseCores`. Additionally, all resources will be returned when the task calls `Reacquire` as this call is a signal to the scheduler that external tools have finished their work and the task can continue running. It does not matter when the resources where allocated - whether it was before or after calling `Yield` - they will all be released. Depending on the scheduling policy, freeing resources on `Reacquire` may prevent deadlocks.

The exact core reservation policy and its interaction with task execution scheduling is still TBD. The pool of resources explicitly allocated by tasks may be completely separate, i.e. MSBuild will not wait until a resource is freed before starting execution of new tasks. Or it may be partially or fully shared to prevent oversubscribing the machine. In general, `ReleaseCores` may cause a transition of a waiting task to a Ready state. And vice-versa, completing a task or calling `Yield` may unblock a pending `RequestCores` call issued by a task.

## Example 1

In a 16-process build of a solution with 30 projects, 16 worker nodes are launched and begin executing work. Most block on dependencies to projects `A`, `B`, `C`, `D`, and `E`, so they don't have tasks running holding resources.

Task `Work` is called in project `A` with 25 inputs. It would like to run as many as possible in parallel. It calls

```C#
int allowedParallelism = BuildEngine8.RequestCores(Inputs.Count); // Inputs.Count == 25
```

and gets up to `16`--the number of cores available to the build overall.

While `A` runs `Work`, projects `B` and `C` run another task `Work2` that also calls `RequestCores` with a high value. Since `Work` in `A` has reserved all cores, the calls in `B` and `C` may return only 1, indicating that the task should not be doing parallel work. Subsequent `RequestCores` may block, waiting on `Work` to release cores (or return).

When `Work` returns, MSBuild automatically returns all resources reserved by the task to the pool. At that time blocked `RequestCores` calls in `Work2` may unblock.

## Implementation

The `RequestCores` and `ReleaseCores` calls are marshaled back to the scheduler via newly introduced `INodePacket` implementations. The scheduler, having full view of the state of the system - i.e. number of build requests running, waiting, yielding, ..., number of cores explicitly allocated by individual tasks using the new API - is free to implement an arbitrary core allocation policy. In the initial implementation the policy will be controlled by a couple of environment variables to make it easy to test different settings.
Expand Up @@ -216,6 +216,8 @@ public partial interface IBuildEngine7 : Microsoft.Build.Framework.IBuildEngine,
}
public partial interface IBuildEngine8 : Microsoft.Build.Framework.IBuildEngine, Microsoft.Build.Framework.IBuildEngine2, Microsoft.Build.Framework.IBuildEngine3, Microsoft.Build.Framework.IBuildEngine4, Microsoft.Build.Framework.IBuildEngine5, Microsoft.Build.Framework.IBuildEngine6, Microsoft.Build.Framework.IBuildEngine7
{
void ReleaseCores(int coresToRelease);
int RequestCores(int requestedCores);
bool ShouldTreatWarningAsError(string warningCode);
}
public partial interface ICancelableTask : Microsoft.Build.Framework.ITask
Expand Down
Expand Up @@ -216,6 +216,8 @@ public partial interface IBuildEngine7 : Microsoft.Build.Framework.IBuildEngine,
}
public partial interface IBuildEngine8 : Microsoft.Build.Framework.IBuildEngine, Microsoft.Build.Framework.IBuildEngine2, Microsoft.Build.Framework.IBuildEngine3, Microsoft.Build.Framework.IBuildEngine4, Microsoft.Build.Framework.IBuildEngine5, Microsoft.Build.Framework.IBuildEngine6, Microsoft.Build.Framework.IBuildEngine7
{
void ReleaseCores(int coresToRelease);
int RequestCores(int requestedCores);
bool ShouldTreatWarningAsError(string warningCode);
}
public partial interface ICancelableTask : Microsoft.Build.Framework.ITask
Expand Down
27 changes: 27 additions & 0 deletions src/Build.UnitTests/BackEnd/BuildRequestEngine_Tests.cs
Expand Up @@ -74,6 +74,8 @@ public MockRequestBuilder()

public event BuildRequestBlockedDelegate OnBuildRequestBlocked;

public event ResourceRequestDelegate OnResourceRequest;

public void BuildRequest(NodeLoggingContext context, BuildRequestEntry entry)
{
Assert.Null(_builderThread); // "Received BuildRequest while one was in progress"
Expand Down Expand Up @@ -171,6 +173,11 @@ public void RaiseRequestBlocked(BuildRequestEntry entry, int blockingId, string
OnBuildRequestBlocked?.Invoke(entry, blockingId, blockingTarget, null);
}

public void RaiseResourceRequest(ResourceRequest request)
{
OnResourceRequest?.Invoke(request);
}

public void ContinueRequest()
{
if (ThrowExceptionOnContinue)
Expand All @@ -180,6 +187,10 @@ public void ContinueRequest()
_continueEvent.Set();
}

public void ContinueRequestWithResources(ResourceResponse response)
{
}

public void CancelRequest()
{
this.BeginCancel();
Expand Down Expand Up @@ -256,6 +267,9 @@ private ProjectInstance CreateStandinProject()
private AutoResetEvent _engineExceptionEvent;
private Exception _engineException_Exception;

private AutoResetEvent _engineResourceRequestEvent;
private ResourceRequest _engineResourceRequest_Request;

private IBuildRequestEngine _engine;
private IConfigCache _cache;
private int _nodeRequestId;
Expand All @@ -272,6 +286,7 @@ public BuildRequestEngine_Tests()
_newRequestEvent = new AutoResetEvent(false);
_newConfigurationEvent = new AutoResetEvent(false);
_engineExceptionEvent = new AutoResetEvent(false);
_engineResourceRequestEvent = new AutoResetEvent(false);

_engine = (IBuildRequestEngine)_host.GetComponent(BuildComponentType.RequestEngine);
_cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache);
Expand All @@ -293,6 +308,7 @@ public void Dispose()
_newRequestEvent.Dispose();
_newConfigurationEvent.Dispose();
_engineExceptionEvent.Dispose();
_engineResourceRequestEvent.Dispose();

_host = null;
}
Expand All @@ -305,6 +321,7 @@ private void ConfigureEngine(IBuildRequestEngine engine)
engine.OnRequestResumed += this.Engine_RequestResumed;
engine.OnStatusChanged += this.Engine_EngineStatusChanged;
engine.OnEngineException += this.Engine_Exception;
engine.OnResourceRequest += this.Engine_ResourceRequest;
}

/// <summary>
Expand Down Expand Up @@ -579,5 +596,15 @@ private void Engine_Exception(Exception e)
_engineException_Exception = e;
_engineExceptionEvent.Set();
}

/// <summary>
/// Callback for event raised when resources are requested.
/// </summary>
/// <param name="request">The resource request</param>
private void Engine_ResourceRequest(ResourceRequest request)
{
_engineResourceRequest_Request = request;
_engineResourceRequestEvent.Set();
}
}
}
15 changes: 15 additions & 0 deletions src/Build.UnitTests/BackEnd/TargetBuilder_Tests.cs
Expand Up @@ -1417,6 +1417,21 @@ void IRequestBuilderCallback.ExitMSBuildCallbackState()
{
}

/// <summary>
/// Empty impl
/// </summary>
int IRequestBuilderCallback.RequestCores(object monitorLockObject, int requestedCores, bool waitForCores)
{
return 0;
}

/// <summary>
/// Empty impl
/// </summary>
void IRequestBuilderCallback.ReleaseCores(int coresToRelease)
{
}

#endregion

/// <summary>
Expand Down
15 changes: 15 additions & 0 deletions src/Build.UnitTests/BackEnd/TargetEntry_Tests.cs
Expand Up @@ -978,6 +978,21 @@ void IRequestBuilderCallback.ExitMSBuildCallbackState()
{
}

/// <summary>
/// Empty impl
/// </summary>
int IRequestBuilderCallback.RequestCores(object monitorLockObject, int requestedCores, bool waitForCores)
{
return 0;
}

/// <summary>
/// Empty impl
/// </summary>
void IRequestBuilderCallback.ReleaseCores(int coresToRelease)
{
}

#endregion

/// <summary>
Expand Down
15 changes: 15 additions & 0 deletions src/Build.UnitTests/BackEnd/TaskBuilder_Tests.cs
Expand Up @@ -755,6 +755,21 @@ void IRequestBuilderCallback.ExitMSBuildCallbackState()
{
}

/// <summary>
/// Empty impl
/// </summary>
int IRequestBuilderCallback.RequestCores(object monitorLockObject, int requestedCores, bool waitForCores)
{
return 0;
}

/// <summary>
/// Empty impl
/// </summary>
void IRequestBuilderCallback.ReleaseCores(int coresToRelease)
{
}

#endregion

#region IRequestBuilderCallback Members
Expand Down
136 changes: 136 additions & 0 deletions src/Build.UnitTests/BackEnd/TaskHost_Tests.cs
Expand Up @@ -15,6 +15,7 @@
using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem;
using System.Threading.Tasks;
using Xunit;
using Shouldly;

namespace Microsoft.Build.UnitTests.BackEnd
{
Expand Down Expand Up @@ -705,6 +706,90 @@ public void TasksGetNoGlobalPropertiesIfNoneSpecified()
mockLogger.AssertLogContains("Global property count: 0");
}

[Fact]
public void RequestCoresThrowsOnInvalidInput()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
_taskHost.RequestCores(0);
});

Assert.Throws<ArgumentOutOfRangeException>(() =>
{
_taskHost.RequestCores(-1);
});
}

[Fact]
public void RequestCoresUsesImplicitCore()
{
// If the request callback has no cores to grant, we still get 1 for the implicit core.
_mockRequestCallback.CoresToGrant = 0;
_taskHost.RequestCores(3).ShouldBe(1);
_mockRequestCallback.LastRequestedCores.ShouldBe(2);
_mockRequestCallback.LastWaitForCores.ShouldBeFalse();
}

[Fact]
public void RequestCoresUsesCoresFromRequestCallback()
{
// The request callback has 1 core to grant, we should see it returned from RequestCores.
_mockRequestCallback.CoresToGrant = 1;
_taskHost.RequestCores(3).ShouldBe(2);
_mockRequestCallback.LastRequestedCores.ShouldBe(2);
_mockRequestCallback.LastWaitForCores.ShouldBeFalse();

// Since we've used the implicit core, the second call will return only what the request callback gives us and may block.
_mockRequestCallback.CoresToGrant = 1;
_taskHost.RequestCores(3).ShouldBe(1);
_mockRequestCallback.LastRequestedCores.ShouldBe(3);
_mockRequestCallback.LastWaitForCores.ShouldBeTrue();
}

[Fact]
public void ReleaseCoresThrowsOnInvalidInput()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
_taskHost.ReleaseCores(0);
});

Assert.Throws<ArgumentOutOfRangeException>(() =>
{
_taskHost.ReleaseCores(-1);
});
}

[Fact]
public void ReleaseCoresReturnsCoresToRequestCallback()
{
_mockRequestCallback.CoresToGrant = 1;
_taskHost.RequestCores(3).ShouldBe(2);

// We return one of two granted cores, the call passes through to the request callback.
_taskHost.ReleaseCores(1);
_mockRequestCallback.LastCoresToRelease.ShouldBe(1);

// The implicit core is still allocated so a subsequent RequestCores call may block.
_taskHost.RequestCores(1);
_mockRequestCallback.LastWaitForCores.ShouldBeTrue();
}

[Fact]
public void ReleaseCoresReturnsImplicitCore()
{
_mockRequestCallback.CoresToGrant = 1;
_taskHost.RequestCores(3).ShouldBe(2);

// We return both granted cores, one of them is returned to the request callback.
_taskHost.ReleaseCores(2);
_mockRequestCallback.LastCoresToRelease.ShouldBe(1);

// The implicit core is not allocated anymore so a subsequent RequestCores call won't block.
_taskHost.RequestCores(1);
_mockRequestCallback.LastWaitForCores.ShouldBeFalse();
}

#region Helper Classes

/// <summary>
Expand Down Expand Up @@ -1221,6 +1306,26 @@ internal class MockIRequestBuilderCallback : IRequestBuilderCallback, IRequestBu
/// </summary>
private BuildResult[] _buildResultsToReturn;

/// <summary>
/// The requestedCores argument passed to the last RequestCores call.
/// </summary>
public int LastRequestedCores { get; private set; }

/// <summary>
/// The waitForCores argument passed to the last RequestCores call.
/// </summary>
public bool LastWaitForCores { get; private set; }

/// <summary>
/// The value to be returned from the RequestCores call.
/// </summary>
public int CoresToGrant { get; set; }

/// <summary>
/// The coresToRelease argument passed to the last ReleaseCores call.
/// </summary>
public int LastCoresToRelease { get; private set; }

/// <summary>
/// Constructor which takes an array of build results to return from the BuildProjects method when it is called.
/// </summary>
Expand All @@ -1247,6 +1352,11 @@ internal MockIRequestBuilderCallback(BuildResult[] buildResultsToReturn)
/// Not Implemented
/// </summary>
public event BuildRequestBlockedDelegate OnBuildRequestBlocked;

/// <summary>
/// Not Implemented
/// </summary>
public event ResourceRequestDelegate OnResourceRequest;
#pragma warning restore

/// <summary>
Expand Down Expand Up @@ -1294,6 +1404,24 @@ public void ExitMSBuildCallbackState()
{
}

/// <summary>
/// Mock
/// </summary>
public int RequestCores(object monitorLockObject, int requestedCores, bool waitForCores)
{
LastRequestedCores = requestedCores;
LastWaitForCores = waitForCores;
return CoresToGrant;
}

/// <summary>
/// Mock
/// </summary>
public void ReleaseCores(int coresToRelease)
{
LastCoresToRelease = coresToRelease;
}

/// <summary>
/// Mock of the Block on target in progress.
/// </summary>
Expand All @@ -1318,6 +1446,14 @@ public void ContinueRequest()
throw new NotImplementedException();
}

/// <summary>
/// Not Implemented
/// </summary>
public void ContinueRequestWithResources(ResourceResponse response)
{
throw new NotImplementedException();
}

/// <summary>
/// Not Implemented
/// </summary>
Expand Down