From 8093a22c3686de7182108c7189b7459cef32d783 Mon Sep 17 00:00:00 2001 From: John Luo Date: Thu, 14 Jan 2021 02:36:33 -0800 Subject: [PATCH 1/4] Ensure watched projects are not built in parallel to avoid file locking issues --- .../Model/Application.cs | 15 +++++++++++++++ src/Microsoft.Tye.Hosting/ProcessRunner.cs | 19 +++++++++++++++++-- .../Watch/DotNetWatcher.cs | 5 +++-- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/Model/Application.cs b/src/Microsoft.Tye.Hosting/Model/Application.cs index 7430a0c3f..a1b63fe8c 100644 --- a/src/Microsoft.Tye.Hosting/Model/Application.cs +++ b/src/Microsoft.Tye.Hosting/Model/Application.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Threading; namespace Microsoft.Tye.Hosting.Model { @@ -17,6 +18,18 @@ public Application(FileInfo source, Dictionary services) Source = source.FullName; ContextDirectory = source.DirectoryName!; Services = services; + + foreach (var s in Services.Values) + { + if (s.Description.RunInfo is ProjectRunInfo projectRunInfo) + { + var projectFileName = projectRunInfo.ProjectFile.FullName; + if (!ProjectFileLocks.ContainsKey(projectFileName)) + { + ProjectFileLocks[projectFileName] = new SemaphoreSlim(1, 1); + } + } + } } public string Source { get; } @@ -25,6 +38,8 @@ public Application(FileInfo source, Dictionary services) public Dictionary Services { get; } + internal Dictionary ProjectFileLocks { get; } = new Dictionary(); + public Dictionary Items { get; } = new Dictionary(); public string? Network { get; set; } diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index 706cfadcc..a912b686a 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -359,9 +359,24 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? }, Build = async () => { - if (service.Description.RunInfo is ProjectRunInfo) + if (service.Description.RunInfo is ProjectRunInfo projectRunInfo) { - var buildResult = await ProcessUtil.RunAsync("dotnet", $"build \"{service.Status.ProjectFilePath}\" /nologo", throwOnError: false, workingDirectory: application.ContextDirectory); + ProcessResult buildResult; + + var projectFile = projectRunInfo.ProjectFile.FullName; + var projectSemaphore = application.ProjectFileLocks[projectFile]; + await projectSemaphore.WaitAsync(); + try + { + _logger.LogDebug($"[{replica}] Building project {projectFile}:"); + buildResult = await ProcessUtil.RunAsync("dotnet", $"build \"{service.Status.ProjectFilePath}\" /nologo", throwOnError: false, workingDirectory: application.ContextDirectory); + } + finally + { + _logger.LogDebug($"[{replica}] Finished Building project {projectFile}:"); + projectSemaphore.Release(); + } + if (buildResult.ExitCode != 0) { _logger.LogInformation("Building projects failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); diff --git a/src/Microsoft.Tye.Hosting/Watch/DotNetWatcher.cs b/src/Microsoft.Tye.Hosting/Watch/DotNetWatcher.cs index b3e89dfea..42b3e1d57 100644 --- a/src/Microsoft.Tye.Hosting/Watch/DotNetWatcher.cs +++ b/src/Microsoft.Tye.Hosting/Watch/DotNetWatcher.cs @@ -88,7 +88,7 @@ public async Task WatchAsync(ProcessSpec processSpec, IFileSetFactory fileSetFac if (finishedTask == processTask) { // Now wait for a file to change before restarting process - await fileSetWatcher.GetChangedFileAsync(cancellationToken, () => _logger.LogWarning("Waiting for a file to change before restarting dotnet...")); + await fileSetWatcher.GetChangedFileAsync(cancellationToken, () => _logger.LogWarning("watch: {Replica} Waiting for a file to change before restarting dotnet...", replica)); } if (!string.IsNullOrEmpty(fileSetTask.Result)) @@ -112,7 +112,8 @@ public async Task WatchAsync(ProcessSpec processSpec, IFileSetFactory fileSetFac // Build failed, keep retrying builds until successful build. } - await fileSetWatcher.GetChangedFileAsync(cancellationToken, () => _logger.LogWarning("Waiting for a file to change before restarting dotnet...")); + // Now wait for a file to change before restarting process + await fileSetWatcher.GetChangedFileAsync(cancellationToken, () => _logger.LogWarning("watch: {Replica} Waiting for a file to change before restarting dotnet...", replica)); } } } From 5cb19d0062137c1cb5f240cf47f370069ed454c1 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 15 Jan 2021 00:16:14 -0800 Subject: [PATCH 2/4] Update implementation to be TCS based --- .../Model/Application.cs | 17 +++--------- src/Microsoft.Tye.Hosting/ProcessRunner.cs | 27 ++++++++++--------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/Model/Application.cs b/src/Microsoft.Tye.Hosting/Model/Application.cs index a1b63fe8c..0fa9f6e47 100644 --- a/src/Microsoft.Tye.Hosting/Model/Application.cs +++ b/src/Microsoft.Tye.Hosting/Model/Application.cs @@ -3,11 +3,12 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Threading; +using System.Threading.Tasks; namespace Microsoft.Tye.Hosting.Model { @@ -18,18 +19,6 @@ public Application(FileInfo source, Dictionary services) Source = source.FullName; ContextDirectory = source.DirectoryName!; Services = services; - - foreach (var s in Services.Values) - { - if (s.Description.RunInfo is ProjectRunInfo projectRunInfo) - { - var projectFileName = projectRunInfo.ProjectFile.FullName; - if (!ProjectFileLocks.ContainsKey(projectFileName)) - { - ProjectFileLocks[projectFileName] = new SemaphoreSlim(1, 1); - } - } - } } public string Source { get; } @@ -38,7 +27,7 @@ public Application(FileInfo source, Dictionary services) public Dictionary Services { get; } - internal Dictionary ProjectFileLocks { get; } = new Dictionary(); + internal ConcurrentDictionary> ProjectProcesses { get; } = new ConcurrentDictionary>(); public Dictionary Items { get; } = new Dictionary(); diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index a912b686a..47f107a57 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -361,22 +361,25 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? { if (service.Description.RunInfo is ProjectRunInfo projectRunInfo) { - ProcessResult buildResult; - var projectFile = projectRunInfo.ProjectFile.FullName; - var projectSemaphore = application.ProjectFileLocks[projectFile]; - await projectSemaphore.WaitAsync(); - try - { - _logger.LogDebug($"[{replica}] Building project {projectFile}:"); - buildResult = await ProcessUtil.RunAsync("dotnet", $"build \"{service.Status.ProjectFilePath}\" /nologo", throwOnError: false, workingDirectory: application.ContextDirectory); - } - finally + var newProcess = new TaskCompletionSource(); + var ongoingProcess = application.ProjectProcesses.GetOrAdd(projectFile, newProcess); + + if (ongoingProcess != newProcess) { - _logger.LogDebug($"[{replica}] Finished Building project {projectFile}:"); - projectSemaphore.Release(); + return (await ongoingProcess.Task).ExitCode; } + _logger.LogDebug($"[{replica}] Building project {projectFile}:"); + var buildResult = await ProcessUtil.RunAsync("dotnet", $"build \"{service.Status.ProjectFilePath}\" /nologo", throwOnError: false, workingDirectory: application.ContextDirectory); + _logger.LogDebug($"[{replica}] Finished Building project {projectFile}:"); + + ongoingProcess.SetResult(buildResult); + + // Cannot remove a specific KVP until net5.0. Workaround is to cast to ICollection> + ICollection>> projectProcesses = application.ProjectProcesses; + projectProcesses.Remove(KeyValuePair.Create(projectFile, ongoingProcess)); + if (buildResult.ExitCode != 0) { _logger.LogInformation("Building projects failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); From 392fc91d3778801c3bfd0e544ac823d2d55ed9ff Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 15 Jan 2021 10:42:21 -0800 Subject: [PATCH 3/4] Naming --- src/Microsoft.Tye.Hosting/Model/Application.cs | 3 ++- src/Microsoft.Tye.Hosting/ProcessRunner.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/Model/Application.cs b/src/Microsoft.Tye.Hosting/Model/Application.cs index 0fa9f6e47..c8c464f04 100644 --- a/src/Microsoft.Tye.Hosting/Model/Application.cs +++ b/src/Microsoft.Tye.Hosting/Model/Application.cs @@ -27,7 +27,8 @@ public Application(FileInfo source, Dictionary services) public Dictionary Services { get; } - internal ConcurrentDictionary> ProjectProcesses { get; } = new ConcurrentDictionary>(); + internal ConcurrentDictionary> OngoingBuildProjectProcesses { get; } + = new ConcurrentDictionary>(); public Dictionary Items { get; } = new Dictionary(); diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index 47f107a57..d529e0dd2 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -363,7 +363,7 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? { var projectFile = projectRunInfo.ProjectFile.FullName; var newProcess = new TaskCompletionSource(); - var ongoingProcess = application.ProjectProcesses.GetOrAdd(projectFile, newProcess); + var ongoingProcess = application.OngoingBuildProjectProcesses.GetOrAdd(projectFile, newProcess); if (ongoingProcess != newProcess) { @@ -377,7 +377,7 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? ongoingProcess.SetResult(buildResult); // Cannot remove a specific KVP until net5.0. Workaround is to cast to ICollection> - ICollection>> projectProcesses = application.ProjectProcesses; + ICollection>> projectProcesses = application.OngoingBuildProjectProcesses; projectProcesses.Remove(KeyValuePair.Create(projectFile, ongoingProcess)); if (buildResult.ExitCode != 0) From d9fa83a871512fd38ff764b9e2f2f396522bad97 Mon Sep 17 00:00:00 2001 From: John Luo Date: Tue, 19 Jan 2021 10:47:51 -0800 Subject: [PATCH 4/4] Add additional logging --- src/Microsoft.Tye.Hosting/ProcessRunner.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index d529e0dd2..b3b47b309 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -367,12 +367,13 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? if (ongoingProcess != newProcess) { + _logger.LogDebug($"[{replica}] A build has already been triggered for project {projectFile}"); return (await ongoingProcess.Task).ExitCode; } - _logger.LogDebug($"[{replica}] Building project {projectFile}:"); + _logger.LogDebug($"[{replica}] Building project {projectFile}"); var buildResult = await ProcessUtil.RunAsync("dotnet", $"build \"{service.Status.ProjectFilePath}\" /nologo", throwOnError: false, workingDirectory: application.ContextDirectory); - _logger.LogDebug($"[{replica}] Finished Building project {projectFile}:"); + _logger.LogDebug($"[{replica}] Finished Building project {projectFile}"); ongoingProcess.SetResult(buildResult);