-
-
Notifications
You must be signed in to change notification settings - Fork 732
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
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
Allow parallel execution of tasks #156
Comments
I'd piggy back on this and also (re?)mention the idea of async tasks. |
Took a stab at this with this spike. Not ideal, but at least it cut down our 8-minute build time down to five. |
A big issue with parallelism is viewing the log output. I came up with a solution that buffers output so that the log looks exactly like the tasks are executed sequentially, but they are in fact simultaneous. As soon as the first action is finished, all the second action's buffered output is dumped immediately and you get to watch the second action continue live- or, if it's also finished, you see the third. Etc. This can be used in your build scripts without modifying Cake. https://gist.github.com/jnm2/a8a39b67a584ad555360102407049ae2 Task("Example")
.Does(() => RunSimultaneously(
() =>
{
StartProcess("ping", new ProcessSettings().WithArguments(a => a.Append("github.com")));
Information("FINISHED 1");
},
() =>
{
RunSimultaneously(() =>
{
StartProcess("ping", new ProcessSettings().WithArguments(a => a.Append("192.123.45.67")));
Information("FINISHED 2");
},
() =>
{
StartProcess("ping", new ProcessSettings().WithArguments(a => a.Append("twitter.com")));
Information("FINISHED 3");
});
}
)); I'd love for some of this to be baked into Cake's dependency graph analysis without even needing to manually specify RunSimultaneously. That would be fantastic! |
might i suggest this for declaring parallel taks. Feel free to ignore if you think it doesn't make sense:
|
I feel like it should be during the dependency graph analysis that it should determine how to best spin off and schedule tasks on their own threads. I feel that introducing any special syntax is unnecessary, as the same task definition should would regardless of parallelization. @jnm2 As soon as I dropped my parallelization module into my project my build logs were almost useless. I think I'll take a cue from your gist and incorporate the output capturing into a buffered console writer. Still pretty satisfied with the gains, though my dependency graph analysis algorithm could definitely use some work. |
Okay so how about this use case: I want to run unit tests and integration tests before building the installer, but both take a while. So what I'd like to do run tests and build the installer at the same time, but cancel building the installer as soon as any test fails. If the installer task finishes before the tests, it should wait for the tests to complete (or fail) before staging the build installer files. |
In pseudocode: var runUnitTests = Task("RunUnitTests").Does(() => ...);
var integrationTestNewDatabase = Task("IntegrationTestNewDatabase").Does(() => ...);
var integrationTestUpgradeDatabase = Task("IntegrationTestUpgradeDatabase").Does(() => ...);
var buildInstaller = Task("BuildInstaller").Does(parallelDependencies =>
{
BuildInstallerStep1();
if (parallelDependencies.HaveFailure) return; // If any fails, let the build end
BuildInstallerStep2();
if (parallelDependencies.HaveFailure) return;
BuildInstallerStep3();
if (!parallelDependencies.WaitForSuccess()) return; // Waits for all to succeed or any to fail
StageFiles();
});
Task("TestAndBuildInstaller")
.DependsOn(runUnitTests)
.DependsOn(integrationTestNewDatabase)
.DependsOn(integrationTestUpgradeDatabase)
.DependsOn(buildInstaller, parallelDependences: new[] {
runUnitTests,
integrationTestNewDatabase,
integrationTestUpgradeDatabase
}); |
Task("stage-installer")
.IsDependentOn("unit-tests")
.IsDependentOn("integration-tests")
.IsDependentOn("build-installer");
Task("ci")
.IsDependentOn("unit-tests")
.IsDependentOn("integration-tests")
.IsDependentOn("build-installer")
.IsDependentOn("stage-installer"); We have a similar setup on the project I dropped my module in. |
@aabenoja Much cleaner! However, no early exit for It's a little weird getting my head around the fact that Tasks would execute in two stages: 1. |
Actually, Task("unit-tests")
.IgnoreCancellation();
Task("integration-tests")
.IgnoreCancellation(); While maybe not ideal, but at least this way the whole build will still fail and we get the full results of these two tasks. It makes sense to me that |
I'm thinking cancellation should be opt-in.
So along those lines, it makes even more sense to me if the dependencies are not parallel unless opted in. Seems the safest approach. |
Which I am. I've been generating task chains for each dependency, sharing tasks where necessary, and passing around a single CancellationToken across all tasks.
Task("nuget-restore").Does(() => {});
Task("compile")
.IsDependentOn("nuget-restore")
.Does(() => {});
Task("unit-tests")
.IsDependentOn("compile")
.Does(() => {});
Task("integration-tests")
.IsDependentOn("compile")
.Does(() => {});
Task("tests")
.IsDependentOn("unit-tests")
.IsDependentOn("integration-tests"); To go more in-depth on what I've done, The whole idea is to execute in parallel where it makes sense. It's possible to throw |
Another example is a mvc web app. Before we create a deployable zip to drop into iis we want to ensure javascript is compiled, javascript tests are passing, our dlls are generated, and xunit tests (both unit and integration) are run. We shouldn't care what order these things and their dependencies occur, just that they happen, no different than when setting up the task without parallelization in mind. If anything, it makes more sense to me to use a cli flag run all tasks sequentially or in parallel. Having a weird mix of parallel and not parallel tasks seems like there's an issue with what is understood as a task's "dependencies." |
That's certainly an opinionated way for Cake to go, but it might be a good thing. |
Counterexample: Task("Clean")
.Does(() => { });
// Priority #1: You want to be able to restore without cleaning
Task("Restore")
.Does(() => { });
// Priority #2: You want to be able to build without cleaning or restoring
Task("Build")
.Does(() => { });
// Priority #3: But never pack without cleaning
// Problem: how do you keep all three dependencies from attempting to run at the same time while preserving priorities 1 and 2?
Task("Pack")
.IsDependentOn("Clean")
.IsDependentOn("Restore")
.IsDependentOn("Build")
.Does(() => { }); |
If the only way to prevent things from running in parallel is to make them dependent on each other, there's no way to preserve priorities 1 and 2 which are very legit priorities. This boilerplate can't help you: Task("CleanAndRestore")
.IsDependentOn("Clean")
.IsDependentOn("Restore"); Neither can this: Task("Clean2")
.IsDependentOn("Clean")
Task("Restore2")
.IsDependentOn("Clean2")
.IsDependentOn("Restore");
// There's still nothing to indicate that "Restore" should wait for "Clean2" to finish
Task("Build2")
.IsDependentOn("Restore2")
.IsDependentOn("Build")
// There's still nothing to indicate that "Build" should wait for "Restore2" to finish In fact, the only way around this problem is to entirely duplicate the definition of the On top of this insurmountable problem, we have the problem that making everything parallel by default is by nature a breaking change. This leads me to think we need a better API to opt in to parallelization with as little boilerplate as possible. Ideally without creating the need for boilerplate tasks. |
So we need to be able to say:
|
Do we want to specify a potentially different parallel relationship between dependencies every time they show up in dependent tasks? It could lead to duplication and inconsistency, but the API is minimalist and perhaps the extra control per dependent over the parallelism of the dependencies will be needed someday: Task("Build")
.Does(() => { });
Task("TestA")
.IsDependentOn("Build")
.Does(() => { });
Task("TestB")
.IsDependentOn("Build")
.Does(() => { });
Task("BuildInstaller")
.IsDependentOn("Build")
.Does(() => { });
Task("AllTests")
.IsDependentOnParallel("TestA", "TestB");
Task("TestAndBuildInstaller")
.IsDependentOnParallel("AllTests", "BuildInstaller"); |
So in this API, the order of the Task("Pack")
.IsDependentOn("Clean")
.IsDependentOn("Restore")
.IsDependentOn("Build")
.IsDependentOnParallel("X", "Y", "Z")
.Does(() => { }); |
@patriksvensson I'm interested in your thoughts. |
Another reason not to parallelize by default is that it messes with logs, and there is a solution for that but it means redirecting the standard output and error of everything you run, losing Windows console colors. |
Personally, I would prefer any sort of parallelization be opt-in as you suggest. There are just too many build scripts out there to turn on something that so fundamentally changes flow without causing major headaches. As an alternative to your API, what if we used an
It's a little more verbose, but to me it's clearer what's going on and better matches the existing API. |
Cool! That's a new angle.
|
Any progress on this topic? I'd like to have such a feature to speed up large project builds. |
I have used https://cakebuild.net/docs/fundamentals/asynchronous-tasks and RunTarget to get this working now. It isn't pretty but does allow multiple chains of tasks to be ran. If there was a nice way for logs that would be even better. Task("__SplitHere")
.Does(async () => {
var dotNetTasks = System.Threading.Tasks.Task.Run(() => RunTarget("__DotNetTasks"));
var clientTasks = System.Threading.Tasks.Task.Run(() => RunTarget("__ClientTasks"));
await System.Threading.Tasks.Task.WhenAll(dotNetTasks, clientTasks);
}); |
@andymac4182 Here's an interim solution for the log issue that might interest you: #156 (comment) |
I pretty much liked the suggestion of @andymac4182 to use RunTarget. I tried it out and It works fine with the exception that each call to
Everything invoked through |
I am currently in the process of trying to run our tasks in parallel as well and also hitting several issues. I did try to use the solution mentioned in #156 (comment) and this does mostly work, although we ran into issues with the Cake.BuildSystems.Module that reported duplicate tasks, which probably has something to do with the fact that RunTarget will just start a new run.
So would it be an idea to add a
This RunTask is mainly different from RunTarget that it wouldn't create a complete new context and doesn't show an output itself, but rather it will kind of manipulate the list of tasks that have to be run. The output of the task in the example above would look like this:
|
Any new thoughts on "how to run tasks in parallel"? Would be awesome to get a statement or roadmap for future Cake evolution. |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
Image the following task graph which start at
1
and ends at5
:Normally, we would do a topographic sort that gives us the order
1 -> 2 -> 3 -> 4 -> 5
, but since this example graph is diamond shaped, we could actually run task2
,3
and4
in parallel, by treating them as a single task which would result in the execution order1 -> (2 | 3 | 4) -> 5
.Since most tasks in a Cake script probably will be I/O bound, this isn't always what you would want, but theoretically this might shave some seconds of a big build script if applied to the right kind of tasks. We could make this functionality opt-in to prevent it from actually being slower.
The text was updated successfully, but these errors were encountered: