Skip to content

Commit 875bedb

Browse files
committed
Support parallel execution of tasks
1 parent c68ebdd commit 875bedb

File tree

11 files changed

+269
-6
lines changed

11 files changed

+269
-6
lines changed

src/Cake.Core.Tests/Unit/CakeEngineTests.cs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,6 +1558,118 @@ public async Task Should_Return_Report_That_Marks_Failed_Tasks_As_Failed()
15581558
Assert.Equal(CakeTaskExecutionStatus.Delegated, report.First(e => e.TaskName == "A").ExecutionStatus);
15591559
Assert.Equal(CakeTaskExecutionStatus.Failed, report.First(e => e.TaskName == "B").ExecutionStatus);
15601560
}
1561+
1562+
[Fact]
1563+
public async Task Should_Throw_Exception_For_Circular_Dependencies()
1564+
{
1565+
// Given
1566+
var fixture = new CakeEngineFixture();
1567+
var settings = new ExecutionSettings().SetTarget("B");
1568+
var engine = fixture.CreateEngine();
1569+
engine.RegisterTask("B").IsDependentOn("C");
1570+
engine.RegisterTask("C").IsDependentOn("D");
1571+
engine.RegisterTask("D").IsDependentOn("B");
1572+
1573+
// When
1574+
var result = await Record.ExceptionAsync(() => engine.RunTargetAsync(fixture.Context, fixture.ExecutionStrategy, settings));
1575+
1576+
// Then
1577+
Assert.IsType<CakeException>(result);
1578+
Assert.Equal("Graph contains circular references.", result?.Message);
1579+
}
1580+
1581+
[Fact]
1582+
public async Task Should_Throw_Exception_For_Circular_Dependencies_In_Parallel()
1583+
{
1584+
// Given
1585+
var fixture = new CakeEngineFixture();
1586+
var settings = new ExecutionSettings().SetTarget("B").RunInParallel();
1587+
var engine = fixture.CreateEngine();
1588+
engine.RegisterTask("B").IsDependentOn("C");
1589+
engine.RegisterTask("C").IsDependentOn("D");
1590+
engine.RegisterTask("D").IsDependentOn("B");
1591+
1592+
// When
1593+
var result = await Record.ExceptionAsync(() => engine.RunTargetAsync(fixture.Context, fixture.ExecutionStrategy, settings));
1594+
1595+
// Then
1596+
Assert.IsType<CakeException>(result);
1597+
Assert.Equal("Graph contains circular references.", result?.Message);
1598+
}
1599+
1600+
[Fact]
1601+
public async Task Should_Execute_Tasks_In_Order_In_Parallel()
1602+
{
1603+
// Given
1604+
var result = new List<string>();
1605+
var fixture = new CakeEngineFixture();
1606+
var settings = new ExecutionSettings().SetTarget("E").RunInParallel();
1607+
var engine = fixture.CreateEngine();
1608+
engine.RegisterTask("A").Does(() => result.Add("A"));
1609+
engine.RegisterTask("B").IsDependentOn("A").Does(() => result.Add("B"));
1610+
engine.RegisterTask("C").IsDependentOn("B").Does(() => result.Add("C"));
1611+
engine.RegisterTask("D").IsDependentOn("C").IsDependeeOf("E").Does(() => { result.Add("D"); });
1612+
engine.RegisterTask("E").Does(() => { result.Add("E"); });
1613+
1614+
// When
1615+
await engine.RunTargetAsync(fixture.Context, fixture.ExecutionStrategy, settings);
1616+
1617+
// Then
1618+
Assert.Equal(5, result.Count);
1619+
Assert.Equal("A", result[0]);
1620+
Assert.Equal("B", result[1]);
1621+
Assert.Equal("C", result[2]);
1622+
Assert.Equal("D", result[3]);
1623+
Assert.Equal("E", result[4]);
1624+
}
1625+
1626+
[Fact]
1627+
public async Task Should_Execute_Tasks_In_Parallel()
1628+
{
1629+
// Given
1630+
var result = new List<string>();
1631+
var fixture = new CakeEngineFixture();
1632+
var settings = new ExecutionSettings().SetTarget("E").RunInParallel();
1633+
var engine = fixture.CreateEngine();
1634+
engine.RegisterTask("A").Does(() => result.Add("A"));
1635+
engine.RegisterTask("B").IsDependentOn("A").Does(async () => { await Task.Delay(20); result.Add("B"); });
1636+
engine.RegisterTask("C").IsDependentOn("A").Does(async () => { await Task.Delay(5); result.Add("C"); });
1637+
engine.RegisterTask("D").IsDependentOn("A").Does(() => result.Add("D"));
1638+
engine.RegisterTask("E").IsDependentOn("B").IsDependentOn("C").IsDependentOn("D").Does(() => result.Add("E"));
1639+
1640+
// When
1641+
await engine.RunTargetAsync(fixture.Context, fixture.ExecutionStrategy, settings);
1642+
1643+
// Then
1644+
Assert.Equal(5, result.Count);
1645+
Assert.Equal("A", result[0]);
1646+
Assert.Equal("D", result[1]);
1647+
Assert.Equal("C", result[2]);
1648+
Assert.Equal("B", result[3]);
1649+
Assert.Equal("E", result[4]);
1650+
}
1651+
1652+
[Fact]
1653+
public async Task Should_Not_Catch_Exceptions_From_Task_If_ContinueOnError_Is_Not_Set_In_Parallel()
1654+
{
1655+
// Given
1656+
var fixture = new CakeEngineFixture();
1657+
var settings = new ExecutionSettings().SetTarget("E").RunInParallel();
1658+
var engine = fixture.CreateEngine();
1659+
engine.RegisterTask("A");
1660+
engine.RegisterTask("B").IsDependentOn("A");
1661+
engine.RegisterTask("C").IsDependentOn("A").Does(() => throw new InvalidOperationException("Whoopsie"));
1662+
engine.RegisterTask("D").IsDependentOn("A");
1663+
engine.RegisterTask("E").IsDependentOn("B").IsDependentOn("C").IsDependentOn("D");
1664+
1665+
// When
1666+
var result = await Record.ExceptionAsync(() =>
1667+
engine.RunTargetAsync(fixture.Context, fixture.ExecutionStrategy, settings));
1668+
1669+
// Then
1670+
Assert.IsType<InvalidOperationException>(result);
1671+
Assert.Equal("Whoopsie", result?.Message);
1672+
}
15611673
}
15621674

15631675
public sealed class TheSetupEvent

src/Cake.Core/CakeEngine.cs

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Globalization;
99
using System.Linq;
1010
using System.Reflection;
11+
using System.Threading;
1112
using System.Threading.Tasks;
1213
using Cake.Core.Diagnostics;
1314
using Cake.Core.Graph;
@@ -211,19 +212,48 @@ public async Task<CakeReport> RunTargetAsync(ICakeContext context, IExecutionStr
211212
{
212213
// Execute only the target task.
213214
var task = _tasks.FirstOrDefault(x => x.Name.Equals(settings.Target, StringComparison.OrdinalIgnoreCase));
214-
await RunTask(context, strategy, task, target, stopWatch, report);
215+
await RunTask(context, strategy, task, target, stopWatch, report, null);
216+
}
217+
else if (settings.Parallel)
218+
{
219+
await graph.TraverseAsync(target, async (taskName, cancellationTokenSource) =>
220+
{
221+
if (cancellationTokenSource.IsCancellationRequested)
222+
{
223+
return;
224+
}
225+
226+
var task = _tasks.FirstOrDefault(_ => _.Name.Equals(taskName, StringComparison.OrdinalIgnoreCase));
227+
Debug.Assert(task != null, "Node should not be null");
228+
229+
var isTarget = task.Name.Equals(target, StringComparison.OrdinalIgnoreCase);
230+
231+
await RunTask(context, strategy, task, target, stopWatch, report, cancellationTokenSource);
232+
});
215233
}
216234
else
217235
{
218236
// Execute all scheduled tasks.
219237
foreach (var task in orderedTasks)
220238
{
221-
await RunTask(context, strategy, task, target, stopWatch, report);
239+
await RunTask(context, strategy, task, target, stopWatch, report, null);
222240
}
223241
}
224242

225243
return report;
226244
}
245+
catch (TaskCanceledException)
246+
{
247+
exceptionWasThrown = true;
248+
throw;
249+
}
250+
catch (AggregateException ex)
251+
{
252+
exceptionWasThrown = true;
253+
thrownException = ex.InnerException;
254+
255+
throw ex.GetBaseException();
256+
}
227257
catch (Exception ex)
228258
{
229259
exceptionWasThrown = true;
@@ -236,7 +266,7 @@ public async Task<CakeReport> RunTargetAsync(ICakeContext context, IExecutionStr
236266
}
237267
}
238268

239-
private async Task RunTask(ICakeContext context, IExecutionStrategy strategy, CakeTask task, string target, Stopwatch stopWatch, CakeReport report)
269+
private async Task RunTask(ICakeContext context, IExecutionStrategy strategy, CakeTask task, string target, Stopwatch stopWatch, CakeReport report, CancellationTokenSource cancellationTokenSource)
240270
{
241271
// Is this the current target?
242272
var isTarget = task.Name.Equals(target, StringComparison.OrdinalIgnoreCase);
@@ -255,7 +285,7 @@ private async Task RunTask(ICakeContext context, IExecutionStrategy strategy, Ca
255285

256286
if (!skipped)
257287
{
258-
await ExecuteTaskAsync(context, strategy, stopWatch, task, report).ConfigureAwait(false);
288+
await ExecuteTaskAsync(context, strategy, stopWatch, task, report, cancellationTokenSource).ConfigureAwait(false);
259289
}
260290
}
261291

@@ -309,7 +339,7 @@ private static bool ShouldTaskExecute(ICakeContext context, CakeTask task, CakeT
309339
}
310340

311341
private async Task ExecuteTaskAsync(ICakeContext context, IExecutionStrategy strategy, Stopwatch stopWatch,
312-
CakeTask task, CakeReport report)
342+
CakeTask task, CakeReport report, CancellationTokenSource cancellationTokenSource)
313343
{
314344
stopWatch.Restart();
315345

@@ -321,6 +351,11 @@ private async Task ExecuteTaskAsync(ICakeContext context, IExecutionStrategy str
321351
// Execute the task.
322352
await strategy.ExecuteAsync(task, context).ConfigureAwait(false);
323353
}
354+
catch (TaskCanceledException exception)
355+
{
356+
taskException = exception;
357+
throw;
358+
}
324359
catch (Exception exception)
325360
{
326361
_log.Error("An error occurred when executing task '{0}'.", task.Name);
@@ -340,6 +375,8 @@ private async Task ExecuteTaskAsync(ICakeContext context, IExecutionStrategy str
340375
}
341376
else
342377
{
378+
cancellationTokenSource?.Cancel();
379+
343380
// No error handler defined for this task.
344381
// Rethrow the exception and let it propagate.
345382
throw;

src/Cake.Core/ExecutionSettings.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ public sealed class ExecutionSettings
1919
/// </summary>
2020
public bool Exclusive { get; private set; }
2121

22+
/// <summary>
23+
/// Gets a value indicating whether the dependend task of the target should be run in parallel (if possible).
24+
/// </summary>
25+
public bool Parallel { get; private set; }
26+
2227
/// <summary>
2328
/// Sets the target to be executed.
2429
/// </summary>
@@ -39,5 +44,15 @@ public ExecutionSettings UseExclusiveTarget()
3944
Exclusive = true;
4045
return this;
4146
}
47+
48+
/// <summary>
49+
/// Whether or not to run the dependend task in parallel.
50+
/// </summary>
51+
/// <returns>The same <see cref="ExecutionSettings"/> instance so that multiple calls can be chained.</returns>
52+
public ExecutionSettings RunInParallel()
53+
{
54+
Parallel = true;
55+
return this;
56+
}
4257
}
4358
}

src/Cake.Core/Graph/CakeGraph.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System;
66
using System.Collections.Generic;
77
using System.Linq;
8+
using System.Threading;
9+
using System.Threading.Tasks;
810

911
namespace Cake.Core.Graph
1012
{
@@ -112,6 +114,29 @@ public IEnumerable<string> Traverse(string target)
112114
return result;
113115
}
114116

117+
/// <summary>
118+
/// Traverses the graph asynchrounus leading to the specified target.
119+
/// </summary>
120+
/// <param name="target">The target to traverse to.</param>
121+
/// <param name="executeTask">Action which will be called on each task.</param>
122+
/// <returns>A task to wait for.</returns>
123+
public async Task TraverseAsync(string target, Func<string, CancellationTokenSource, Task> executeTask)
124+
{
125+
if (!Exist(target))
126+
{
127+
return;
128+
}
129+
130+
if (HasCircularReferences(target))
131+
{
132+
throw new CakeException("Graph contains circular references.");
133+
}
134+
135+
var cancellationTokenSource = new CancellationTokenSource();
136+
var visitedNodes = new Dictionary<string, Task>();
137+
await TraverseAsync(target, executeTask, cancellationTokenSource, visitedNodes);
138+
}
139+
115140
private void Traverse(string node, ICollection<string> result, ISet<string> visited = null)
116141
{
117142
visited = visited ?? new HashSet<string>(StringComparer.OrdinalIgnoreCase);
@@ -130,5 +155,59 @@ private void Traverse(string node, ICollection<string> result, ISet<string> visi
130155
throw new CakeException("Graph contains circular references.");
131156
}
132157
}
158+
159+
private async Task TraverseAsync(string node, Func<string, CancellationTokenSource, Task> executeTask,
160+
CancellationTokenSource cancellationTokenSource, IDictionary<string, Task> visitedNodes)
161+
{
162+
if (visitedNodes.ContainsKey(node))
163+
{
164+
await visitedNodes[node];
165+
return;
166+
}
167+
168+
var token = cancellationTokenSource.Token;
169+
var dependentTasks = _edges
170+
.Where(x => x.End.Equals(node, StringComparison.OrdinalIgnoreCase))
171+
.Select(x =>
172+
{
173+
var task = TraverseAsync(x.Start, executeTask, cancellationTokenSource, visitedNodes);
174+
visitedNodes[x.Start] = task;
175+
176+
if (task.IsFaulted)
177+
{
178+
throw task.Exception;
179+
}
180+
181+
return task;
182+
})
183+
.ToArray();
184+
185+
if (dependentTasks.Any())
186+
{
187+
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
188+
token.Register(() => tcs.TrySetCanceled(), false);
189+
await Task.WhenAny(Task.WhenAll(dependentTasks), tcs.Task);
190+
}
191+
192+
await executeTask(node, cancellationTokenSource);
193+
}
194+
195+
private bool HasCircularReferences(string node, Stack<string> visited = null)
196+
{
197+
visited = visited ?? new Stack<string>();
198+
199+
if (visited.Contains(node))
200+
{
201+
return true;
202+
}
203+
204+
visited.Push(node);
205+
var hasCircularReference = _edges
206+
.Where(x => x.End.Equals(node, StringComparison.OrdinalIgnoreCase))
207+
.Any(x => HasCircularReferences(x.Start, visited));
208+
visited.Pop();
209+
210+
return hasCircularReference;
211+
}
133212
}
134213
}

src/Cake.Frosting/Internal/Commands/DefaultCommand.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ public override int Execute(CommandContext context, DefaultCommandSettings setti
6868
runner.Settings.UseExclusiveTarget();
6969
}
7070

71+
if (settings.Parallel)
72+
{
73+
runner.Settings.RunInParallel();
74+
}
75+
7176
runner.Run(settings.Target);
7277
}
7378
catch (Exception ex)

src/Cake.Frosting/Internal/Commands/DefaultCommandSettings.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,9 @@ internal sealed class DefaultCommandSettings : CommandSettings
5151
[CommandOption("--info")]
5252
[Description("Displays additional information about Cake.")]
5353
public bool Info { get; set; }
54+
55+
[CommandOption("--parallel|-p")]
56+
[Description("Enables the support for parallel tasks.")]
57+
public bool Parallel { get; set; }
5458
}
5559
}

src/Cake.Tests/Unit/ProgramTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Linq;
21
using System.Threading.Tasks;
32
using Autofac;
43
using Cake.Cli;
@@ -32,6 +31,7 @@ public async Task Should_Use_Default_Parameters_By_Default()
3231
settings.BuildHostKind == BuildHostKind.Build &&
3332
settings.Debug == false &&
3433
settings.Exclusive == false &&
34+
settings.Parallel == false &&
3535
settings.Script.FullPath == "build.cake" &&
3636
settings.Verbosity == Verbosity.Normal &&
3737
settings.NoBootstrapping == false));

src/Cake/Commands/DefaultCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public override int Execute(CommandContext context, DefaultCommandSettings setti
7575
Script = settings.Script,
7676
Verbosity = settings.Verbosity,
7777
Exclusive = settings.Exclusive,
78+
Parallel = settings.Parallel,
7879
Debug = settings.Debug,
7980
NoBootstrapping = settings.SkipBootstrap,
8081
});

0 commit comments

Comments
 (0)