Skip to content
This repository has been archived by the owner on Nov 20, 2023. It is now read-only.

Build queue for --watch #1189

Merged
merged 20 commits into from
Feb 9, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions src/Microsoft.Tye.Core/ApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ public ApplicationBuilder(FileInfo source, string name, ContainerEngine containe
public List<IngressBuilder> Ingress { get; } = new List<IngressBuilder>();

public string? Network { get; set; }
public string? BuildSolution { get; internal set; }
}
}
3 changes: 2 additions & 1 deletion src/Microsoft.Tye.Core/ApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public static async Task<ApplicationBuilder> CreateAsync(OutputContext output, F

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>()));
Expand Down
2 changes: 2 additions & 0 deletions src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class ConfigApplication

public int? DashboardPort { get; set; }

public string? BuildSolution { get; set; }

public string? Namespace { get; set; }

public string? Registry { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.Tye.Core/MsBuild/SolutionFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ internal string SolutionFileDirectory

#region Methods

internal bool ProjectShouldBuild(string projectFile)
public bool ProjectShouldBuild(string projectFile)
{
return _solutionFilter?.Contains(FileUtilities.FixFilePath(projectFile)) != false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public static void HandleConfigApplication(YamlMappingNode yamlMappingNode, Conf
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;
Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.Tye.Hosting/Model/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public Application(string name, FileInfo source, int? dashboardPort, Dictionary<
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")
{
Expand Down
13 changes: 7 additions & 6 deletions src/Microsoft.Tye.Hosting/ProcessRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,19 @@ public class ProcessRunner : IApplicationProcessor
private readonly ProcessRunnerOptions _options;
private readonly ReplicaRegistry _replicaRegistry;

private WatchBuilderWorker _watchBuilderWorker;

public ProcessRunner(ILogger logger, ReplicaRegistry replicaRegistry, ProcessRunnerOptions options)
{
_logger = logger;
_replicaRegistry = replicaRegistry;
_options = options;
_watchBuilderWorker = new WatchBuilderWorker(logger);
}

public async Task StartAsync(Application application)
{
_watchBuilderWorker.SolutionPath = application.BuildSolution;
await PurgeFromPreviousRun();

await BuildAndRunProjects(application);
Expand Down Expand Up @@ -356,12 +360,9 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string?
{
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!, application.ContextDirectory);
_logger.LogInformation($"Built {service.Status.ProjectFilePath} with exit code {exitCode}");
return exitCode;
}

return 0;
Expand Down
180 changes: 180 additions & 0 deletions src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Build.Construction;

namespace Microsoft.Tye.Hosting
{
class WatchBuilderWorker
{
private readonly ILogger _logger;
private readonly Channel<BuildRequest> _queue;
private Task _processor;
private string? _solutionPath;

public string? SolutionPath { get => _solutionPath; set => _solutionPath = value; }

public WatchBuilderWorker(ILogger logger)
{
_logger = logger;
_queue = Channel.CreateUnbounded<BuildRequest>();
_processor = Task.Run(ProcessTaskQueueAsync);
}

class BuildRequest
{
public BuildRequest(string projectFilePath, string workingDirectory)
{
this.projectFilePath = projectFilePath;
this.workingDirectory = workingDirectory;
}

public string projectFilePath { get; set; }

public string workingDirectory { get; set; }

private TaskCompletionSource<int> _result = new TaskCompletionSource<int>();

public Task<int> task()
{
return _result.Task;
}

public void complete(int exitCode)
{
if(!_result.TrySetResult(exitCode))
throw new Exception("failed to set result");
}
}

public Task<int> buildProjectFileAsync(string projectFilePath, string workingDirectory) {
var buildRequest = new BuildRequest(projectFilePath, workingDirectory);
_queue.Writer.WriteAsync(buildRequest);
return buildRequest.task();
}

public Task<int> buildProjectFileAsyncImpl(string projectFilePath, string workingDirectory) {
_logger.LogInformation($"Building project ${projectFilePath}...");
return ProcessUtil.RunAsync("dotnet", $"build \"{projectFilePath}\" /nologo", throwOnError: false, workingDirectory: workingDirectory)
.ContinueWith((processTask) => {
var buildResult = processTask.Result;
if (buildResult.ExitCode != 0)
{
_logger.LogInformation("Building projects failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode);
}
return buildResult.ExitCode;
});
}

private string GetProjectName(SolutionFile solution, string projectFile)
{
foreach(var project in solution.ProjectsInOrder) {
if(project.AbsolutePath == projectFile)
{
return project.ProjectName;
}
}
// TODO: error
return "";
}

private async Task ProcessTaskQueueAsync()
{
try
{
while (await _queue.Reader.WaitToReadAsync())
{
var solutionBatch = new Dictionary<string, List<BuildRequest>>(); // store the list of promises
var projectBatch = new Dictionary<string, List<BuildRequest>>();
// TODO: quiet time... maybe wait both...?
await Task.Delay(100);
Copy link
Member

Choose a reason for hiding this comment

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

Why is there a delay here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had the idea of having a quiet time where we wait to see if more files will be saved, then schedule the builds at once... it would be better to have it waiting 100ms (or a more sensible duration) after the last WaitToReadAsync(). Unsure what would happen if it was removed; might still need to yield to let duplicate project build requests enter the queue?


var solution = (SolutionPath != null) ? SolutionFile.Parse(SolutionPath) : null;
string targets = "";
string workingDirectory = ""; // FIXME: should be set in the worker constructor
while (_queue.Reader.TryRead(out BuildRequest item))
{
try {
if(workingDirectory.Length == 0)
{
workingDirectory = item.workingDirectory;
}
if(solution != null && solution.ProjectShouldBuild(item.projectFilePath))
{
if(!solutionBatch.ContainsKey(item.projectFilePath))
{
if(targets.Length > 0)
{
targets += ",";
}
targets += GetProjectName(solution, item.projectFilePath); // note, assuming the default target is Build
solutionBatch.Add(item.projectFilePath, new List<BuildRequest>());
}
solutionBatch[item.projectFilePath].Add(item);
}
else
{
// this will also prevent us building multiple times if a project is used by multiple services
if(!projectBatch.ContainsKey(item.projectFilePath))
{
projectBatch.Add(item.projectFilePath, new List<BuildRequest>());
}
projectBatch[item.projectFilePath].Add(item);
}
}
catch (Exception)
{
item.complete(-1);
}
}

var tasks = new List<Task>();
if(solutionBatch.Count > 0)
{
tasks.Add(Task.Run(async () => {
_logger.LogInformation("Building projects from solution: " + targets);
var buildResult = await ProcessUtil.RunAsync("dotnet", $"msbuild {SolutionPath} -target:{targets}", throwOnError: false, workingDirectory: workingDirectory);
if (buildResult.ExitCode != 0)
{
_logger.LogInformation("Building solution failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode);
}
foreach(var project in solutionBatch)
{
foreach(var buildRequest in project.Value)
{
buildRequest.complete(buildResult.ExitCode);
}
}
}));
}
else
{
foreach(var project in projectBatch)
{
// FIXME: this is serial
tasks.Add(Task.Run(async () => {
var exitCode = await buildProjectFileAsyncImpl(project.Key, workingDirectory);
foreach(var buildRequest in project.Value)
{
buildRequest.complete(exitCode);
}
}));
}
}

Task.WaitAll(tasks.ToArray());
}
}
catch (OperationCanceledException)
{
// Prevent throwing if stoppingToken was signaled
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred executing task work item.");
}
}
}
}
6 changes: 5 additions & 1 deletion src/tye/ApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,11 @@ public static Application ToHostingApplication(this ApplicationBuilder applicati
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)
Expand Down