Browse Source

Build queue for --watch (#1189)

* Central build queue, w/ build from solution

* Fix some warnings

* Need to use -target and not -targets

Also needed to update msbuild version past 16.10 because of a bug that
meant that the default targets of projects couldn't be built.

* Remove unnecessary function

* Handle failures building solution better

* Styling updates.

* Scaffold explicit start/stop of builds.

* More styling updates.

* Cleanup working directory.

* Make build result more consistent.

* Tweak logging.

* Update formatting.

* Update more formatting.

* Add locks for critical areas.

* Update schema and reference doc.

* Fix formatting.

* Updates per PR feedback.

* More updates per PR feedback.

Co-authored-by: James Lloyd <james.lloyd@onlineseminar.nl>
Co-authored-by: phoff <phoff@microsoft.com>
pull/1299/head
James Lloyd 4 years ago
committed by GitHub
parent
commit
f677a157cf
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      docs/reference/schema.md
  2. 1
      src/Microsoft.Tye.Core/ApplicationBuilder.cs
  3. 3
      src/Microsoft.Tye.Core/ApplicationFactory.cs
  4. 2
      src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs
  5. 2
      src/Microsoft.Tye.Core/MsBuild/SolutionFile.cs
  6. 3
      src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs
  7. 293
      src/Microsoft.Tye.Hosting/BuildWatcher.cs
  8. 1
      src/Microsoft.Tye.Hosting/Model/Application.cs
  9. 21
      src/Microsoft.Tye.Hosting/ProcessRunner.cs
  10. 4
      src/schema/tye-schema.json
  11. 6
      src/tye/ApplicationBuilderExtensions.cs

4
docs/reference/schema.md

@ -101,6 +101,10 @@ Specifies the list of ingresses.
Specifies the list of services. Applications must have at least one service.
#### `solution` (string)
Indicates the solution file (.sln) or filter (.slnf) to use when building project-based services in watch mode. If omitted, those services will be built individually. Specifying the solution [filter] can help reduce repeated builds of shared libraries when in watch mode.
## Service
`Service` elements appear in a list within the `services` root property.

1
src/Microsoft.Tye.Core/ApplicationBuilder.cs

@ -36,5 +36,6 @@ namespace Microsoft.Tye
public List<IngressBuilder> Ingress { get; } = new List<IngressBuilder>();
public string? Network { get; set; }
public string? BuildSolution { get; internal set; }
}
}

3
src/Microsoft.Tye.Core/ApplicationFactory.cs

@ -32,7 +32,8 @@ namespace Microsoft.Tye
var root = new ApplicationBuilder(source, rootConfig.Name!, new ContainerEngine(rootConfig.ContainerEngineType), rootConfig.DashboardPort)
{
Namespace = rootConfig.Namespace
Namespace = rootConfig.Namespace,
BuildSolution = rootConfig.BuildSolution,
};
queue.Enqueue((rootConfig, new HashSet<string>()));

2
src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs

@ -26,6 +26,8 @@ namespace Microsoft.Tye.ConfigModel
public int? DashboardPort { get; set; }
public string? BuildSolution { get; set; }
public string? Namespace { get; set; }
public string? Registry { get; set; }

2
src/Microsoft.Tye.Core/MsBuild/SolutionFile.cs

@ -239,7 +239,7 @@ namespace Microsoft.Build.Construction
#region Methods
internal bool ProjectShouldBuild(string projectFile)
public bool ProjectShouldBuild(string projectFile)
{
return _solutionFilter?.Contains(FileUtilities.FixFilePath(projectFile)) != false;
}

3
src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs

@ -21,6 +21,9 @@ namespace Tye.Serialization
case "name":
app.Name = YamlParser.GetScalarValue(key, child.Value);
break;
case "solution":
app.BuildSolution = YamlParser.GetScalarValue(key, child.Value);
break;
case "namespace":
app.Namespace = YamlParser.GetScalarValue(key, child.Value);
break;

293
src/Microsoft.Tye.Hosting/BuildWatcher.cs

@ -0,0 +1,293 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.Build.Construction;
using Microsoft.Extensions.Logging;
namespace Microsoft.Tye.Hosting
{
internal sealed class BuildWatcher : IAsyncDisposable
{
private CancellationTokenSource? _cancellationTokenSource;
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
private readonly ILogger _logger;
private Task? _processor;
private Channel<BuildRequest>? _queue;
public BuildWatcher(ILogger logger)
{
_logger = logger;
}
public Task StartAsync(string? solutionPath, string workingDirectory)
{
return WithLockAsync(
async () =>
{
await ResetAsync();
_queue = Channel.CreateUnbounded<BuildRequest>();
_cancellationTokenSource = new CancellationTokenSource();
_processor = Task.Run(() => ProcessTaskQueueAsync(_logger, _queue.Reader, solutionPath, workingDirectory, _cancellationTokenSource.Token));
});
}
public Task StopAsync()
{
return WithLockAsync(ResetAsync);
}
public async Task<int> BuildProjectFileAsync(string projectFilePath)
{
var request = new BuildRequest(projectFilePath);
await WithLockAsync(
async () =>
{
if (_queue == null)
{
throw new InvalidOperationException("The worker is not running.");
}
await _queue.Writer.WriteAsync(request);
});
return await request.Task;
}
#region IAsyncDisposable Members
public async ValueTask DisposeAsync()
{
await WithLockAsync(ResetAsync);
_lock.Dispose();
}
#endregion
private async Task WithLockAsync(Func<Task> action)
{
await _lock.WaitAsync();
try
{
await action();
}
finally
{
_lock.Release();
}
}
private async Task ResetAsync()
{
if (_queue != null)
{
_queue.Writer.TryComplete();
_queue = null;
}
if (_cancellationTokenSource != null)
{
_cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose();
_cancellationTokenSource = null;
}
if (_processor != null)
{
await _processor;
_processor = null;
}
}
private static string GetProjectName(SolutionFile solution, string projectFile)
{
foreach (var project in solution.ProjectsInOrder)
{
if (project.AbsolutePath == projectFile)
{
return project.ProjectName;
}
}
throw new InvalidOperationException($"Could not find project in solution: {projectFile}");
}
private static async Task ProcessTaskQueueAsync(
ILogger logger,
ChannelReader<BuildRequest> requestReader,
string? solutionPath,
string workingDirectory,
CancellationToken cancellationToken)
{
logger.LogInformation("Build Watcher: Watching for builds...");
try
{
while (await requestReader.WaitToReadAsync(cancellationToken))
{
var delay = TimeSpan.FromMilliseconds(250);
logger.LogInformation("Build Watcher: Builds requested; waiting {DelayInMs}ms for more...", delay.TotalMilliseconds);
await Task.Delay(delay);
logger.LogInformation("Build Watcher: Getting requests...");
var requests = new List<BuildRequest>();
while (requestReader.TryRead(out var request))
{
requests.Add(request);
}
logger.LogInformation("Build Watcher: Processing {Count} requests...", requests.Count);
var solution = (solutionPath != null) ? SolutionFile.Parse(solutionPath) : null;
var solutionBatch = new Dictionary<string, List<BuildRequest>>(); // store the list of promises
var projectBatch = new Dictionary<string, List<BuildRequest>>();
foreach (var request in requests)
{
if (solution?.ProjectShouldBuild(request.ProjectFilePath) == true)
{
if (!solutionBatch.ContainsKey(request.ProjectFilePath))
{
solutionBatch.Add(request.ProjectFilePath, new List<BuildRequest>());
}
solutionBatch[request.ProjectFilePath].Add(request);
}
else
{
// this will also prevent us building multiple times if a project is used by multiple services
if (!projectBatch.ContainsKey(request.ProjectFilePath))
{
projectBatch.Add(request.ProjectFilePath, new List<BuildRequest>());
}
projectBatch[request.ProjectFilePath].Add(request);
}
}
async Task WithRequestCompletion(IEnumerable<BuildRequest> requests, Func<Task<int>> buildFunc)
{
try
{
int exitCode = await buildFunc();
foreach (var request in requests)
{
request.Complete(exitCode);
}
}
catch (Exception ex)
{
foreach (var request in requests)
{
request.Complete(ex);
}
}
}
var tasks = new List<Task>();
if (solutionBatch.Any())
{
var targets = String.Join(",", solutionBatch.Keys.Select(key => GetProjectName(solution!, key)));
tasks.Add(
WithRequestCompletion(
solutionBatch.Values.SelectMany(x => x),
async () =>
{
logger.LogInformation("Build Watcher: Building {Targets} of solution {SolutionPath}...", targets, solutionPath);
var buildResult = await ProcessUtil.RunAsync("dotnet", $"msbuild {solutionPath} -target:{targets}", throwOnError: false, workingDirectory: workingDirectory, cancellationToken: cancellationToken);
if (buildResult.ExitCode != 0)
{
logger.LogInformation("Build Watcher: Solution build failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode);
}
return buildResult.ExitCode;
}));
}
foreach (var project in projectBatch)
{
tasks.Add(
WithRequestCompletion(
project.Value,
async () =>
{
logger.LogInformation("Build Watcher: Building project {ProjectPath}...", project.Key);
var buildResult = await ProcessUtil.RunAsync("dotnet", $"build \"{project.Key}\" /nologo", throwOnError: false, workingDirectory: workingDirectory, cancellationToken: cancellationToken);
if (buildResult.ExitCode != 0)
{
logger.LogInformation("Build Watcher: Project build failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode);
}
return buildResult.ExitCode;
}));
}
logger.LogInformation("Build Watcher: Waiting for builds to complete...");
// NOTE: WithRequestCompletion() will trap exceptions so build errors should not bubble up from WhenAll().
await Task.WhenAll(tasks);
logger.LogInformation("Build Watcher: Done with requests; waiting for more...");
}
}
catch (OperationCanceledException)
{
// NO-OP: Trap exception due to cancellation.
}
catch (Exception ex)
{
logger.LogError(ex, "Build Watcher: Error while processing builds.");
}
logger.LogInformation("Build Watcher: Done watching.");
}
private class BuildRequest
{
private readonly TaskCompletionSource<int> _result = new TaskCompletionSource<int>();
public BuildRequest(string projectFilePath)
{
ProjectFilePath = projectFilePath;
}
public string ProjectFilePath { get; }
public Task<int> Task => _result.Task;
public void Complete(int exitCode)
{
_result.TrySetResult(exitCode);
}
public void Complete(Exception ex)
{
_result.TrySetException(ex);
}
}
}
}

1
src/Microsoft.Tye.Hosting/Model/Application.cs

@ -39,6 +39,7 @@ namespace Microsoft.Tye.Hosting.Model
public Dictionary<object, object> Items { get; } = new Dictionary<object, object>();
public string? Network { get; set; }
public string? BuildSolution { get; set; }
public void PopulateEnvironment(Service service, Action<string, string> set, string defaultHost = "localhost")
{

21
src/Microsoft.Tye.Hosting/ProcessRunner.cs

@ -26,23 +26,31 @@ namespace Microsoft.Tye.Hosting
private readonly ProcessRunnerOptions _options;
private readonly ReplicaRegistry _replicaRegistry;
private readonly BuildWatcher _watchBuilderWorker;
public ProcessRunner(ILogger logger, ReplicaRegistry replicaRegistry, ProcessRunnerOptions options)
{
_logger = logger;
_replicaRegistry = replicaRegistry;
_options = options;
_watchBuilderWorker = new BuildWatcher(logger);
}
public async Task StartAsync(Application application)
{
await PurgeFromPreviousRun();
await _watchBuilderWorker.StartAsync(application.BuildSolution, application.ContextDirectory);
await BuildAndRunProjects(application);
}
public Task StopAsync(Application application)
public async Task StopAsync(Application application)
{
return KillRunningProcesses(application.Services);
await _watchBuilderWorker.StopAsync();
await KillRunningProcesses(application.Services);
}
private async Task BuildAndRunProjects(Application application)
@ -358,12 +366,9 @@ namespace Microsoft.Tye.Hosting
{
if (service.Description.RunInfo is ProjectRunInfo)
{
var buildResult = await ProcessUtil.RunAsync("dotnet", $"build \"{service.Status.ProjectFilePath}\" /nologo", throwOnError: false, workingDirectory: application.ContextDirectory);
if (buildResult.ExitCode != 0)
{
_logger.LogInformation("Building projects failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode);
}
return buildResult.ExitCode;
var exitCode = await _watchBuilderWorker!.BuildProjectFileAsync(service.Status.ProjectFilePath!);
_logger.LogInformation($"Built {service.Status.ProjectFilePath} with exit code {exitCode}");
return exitCode;
}
return 0;

4
src/schema/tye-schema.json

@ -45,6 +45,10 @@
"$ref": "#/definitions/extension"
}
},
"solution": {
"description": "Indicates the solution file (.sln) or filter (.slnf) to use when building project-based services in watch mode. If omitted, those services will be built individually. Specifying the solution [filter] can help reduce repeated builds of shared libraries when in watch mode.",
"type": "string"
},
"services": {
"description": "The application's services.",
"type": "array",

6
src/tye/ApplicationBuilderExtensions.cs

@ -221,7 +221,11 @@ namespace Microsoft.Tye
services.Add(ingress.Name, new Service(description, ServiceSource.Host));
}
return new Application(application.Name, application.Source, application.DashboardPort, services, application.ContainerEngine) { Network = application.Network };
return new Application(application.Name, application.Source, application.DashboardPort, services, application.ContainerEngine)
{
Network = application.Network,
BuildSolution = application.BuildSolution
};
}
public static Tye.Hosting.Model.EnvironmentVariable ToHostingEnvironmentVariable(this EnvironmentVariableBuilder builder)

Loading…
Cancel
Save