From f0bf09665d1749458cfa43265e8c14169e298fee Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sat, 17 Aug 2024 20:27:10 -0400 Subject: [PATCH 1/4] Fix CI Pipeline Badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ea51ba7f332..a646eda7c05 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # tgstation-server -![CI Pipeline](https://github.com/tgstation/tgstation-server/workflows/CI%20Pipeline/badge.svg) [![codecov](https://codecov.io/gh/tgstation/tgstation-server/branch/master/graph/badge.svg)](https://codecov.io/gh/tgstation/tgstation-server) +[![CI Pipeline](https://github.com/tgstation/tgstation-server/actions/workflows/ci-pipeline.yml/badge.svg)](https://github.com/tgstation/tgstation-server/actions/workflows/ci-pipeline.yml) [![codecov](https://codecov.io/gh/tgstation/tgstation-server/branch/master/graph/badge.svg)](https://codecov.io/gh/tgstation/tgstation-server) [![GitHub license](https://img.shields.io/github/license/tgstation/tgstation-server.svg)](LICENSE) [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/tgstation/tgstation-server.svg)](http://isitmaintained.com/project/tgstation/tgstation-server "Average time to resolve an issue") [![NuGet version](https://img.shields.io/nuget/v/Tgstation.Server.Api.svg)](https://www.nuget.org/packages/Tgstation.Server.Api) [![NuGet version](https://img.shields.io/nuget/v/Tgstation.Server.Client.svg)](https://www.nuget.org/packages/Tgstation.Server.Client) From df43a072b29fc35c3825dbce230b34880f943056 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 18 Aug 2024 00:00:02 -0400 Subject: [PATCH 2/4] Fix issues with `JobProgressReporter` --- .../Components/Engine/ByondInstallerBase.cs | 2 +- .../Engine/DelegatingEngineInstaller.cs | 2 +- .../Components/Engine/EngineInstallerBase.cs | 2 +- .../Components/Engine/EngineManager.cs | 81 ++++---- .../Components/Engine/IEngineInstaller.cs | 4 +- .../Components/Engine/IEngineManager.cs | 4 +- .../Components/Engine/OpenDreamInstaller.cs | 61 ++++-- .../Components/Repository/IRepository.cs | 8 +- .../Repository/IRepositoryManager.cs | 4 +- .../Components/Repository/Repository.cs | 190 +++++++++++------ .../Repository/RepositoryManager.cs | 6 +- .../Repository/RepositoryUpdateService.cs | 191 ++++++++++-------- .../Controllers/EngineController.cs | 3 +- .../Extensions/FetchOptionsExtensions.cs | 8 +- .../Jobs/JobProgressReporter.cs | 116 ++++++++++- src/Tgstation.Server.Host/Jobs/JobService.cs | 10 +- .../Engine/TestOpenDreamInstaller.cs | 5 +- .../Repository/TestRepositoryManager.cs | 3 +- .../Jobs/TestJobProgressReporter.cs | 66 ++++++ .../Live/Instance/InstanceTest.cs | 3 +- 20 files changed, 524 insertions(+), 245 deletions(-) create mode 100644 tests/Tgstation.Server.Host.Tests/Jobs/TestJobProgressReporter.cs diff --git a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs index c7c73e3a231..40fbde1a40c 100644 --- a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs +++ b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs @@ -146,7 +146,7 @@ await IOManager.DeleteDirectory( } /// - public override async ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? progressReporter, CancellationToken cancellationToken) + public override async ValueTask DownloadVersion(EngineVersion version, JobProgressReporter progressReporter, CancellationToken cancellationToken) { CheckVersionValidity(version); diff --git a/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs index 91887e73144..c24eee3e8b0 100644 --- a/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs @@ -37,7 +37,7 @@ public IEngineInstallation CreateInstallation(EngineVersion version, string path => DelegateCall(version, installer => installer.CreateInstallation(version, path, installationTask)); /// - public ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken) + public ValueTask DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken) => DelegateCall(version, installer => installer.DownloadVersion(version, jobProgressReporter, cancellationToken)); /// diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs b/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs index 12cf8e657cd..6ca2b940306 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs @@ -52,7 +52,7 @@ protected EngineInstallerBase(IIOManager ioManager, ILogger public abstract ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken); /// - public abstract ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken); + public abstract ValueTask DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken); /// public abstract ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs b/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs index f2a60ae5b9f..970e3ab89d5 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs @@ -118,7 +118,7 @@ public EngineManager(IIOManager ioManager, IEngineInstaller engineInstaller, IEv /// public async ValueTask ChangeVersion( - JobProgressReporter? progressReporter, + JobProgressReporter progressReporter, EngineVersion version, Stream? customVersionStream, bool allowInstallation, @@ -166,8 +166,11 @@ public async ValueTask UseExecutables(EngineVersion? requ "Acquiring lock on BYOND version {version}...", requiredVersion?.ToString() ?? $"{ActiveVersion} (active)"); var versionToUse = requiredVersion ?? ActiveVersion ?? throw new JobException(ErrorCode.EngineNoVersionsInstalled); + + using var progressReporter = new JobProgressReporter(); + var installLock = await AssertAndLockVersion( - null, + progressReporter, versionToUse, null, requiredVersion != null, @@ -388,7 +391,7 @@ await ValueTaskExtensions.WhenAll( /// /// Ensures a BYOND is installed if it isn't already. /// - /// The optional for the operation. + /// The for the operation. /// The to install. /// Optional custom zip file to use. Will cause a number to be added. /// If this BYOND version is required as part of a locking operation. @@ -396,7 +399,7 @@ await ValueTaskExtensions.WhenAll( /// The for the operation. /// A resulting in the . async ValueTask AssertAndLockVersion( - JobProgressReporter? progressReporter, + JobProgressReporter progressReporter, EngineVersion version, Stream? customVersionStream, bool neededForLock, @@ -443,8 +446,7 @@ async ValueTask AssertAndLockVersion( { if (installedOrInstalling) { - if (progressReporter != null) - progressReporter.StageName = "Waiting for existing installation job..."; + progressReporter.StageName = "Waiting for existing installation job..."; if (neededForLock && !installation.InstallationTask.IsCompleted) logger.LogWarning("The required engine version ({version}) is not readily available! We will have to wait for it to install.", version); @@ -468,8 +470,7 @@ async ValueTask AssertAndLockVersion( else logger.LogInformation("Requested engine version {version} not currently installed. Doing so now...", version); - if (progressReporter != null) - progressReporter.StageName = "Running event"; + progressReporter.StageName = "Running event"; var versionString = version.ToString(); await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List { versionString }, deploymentPipelineProcesses, cancellationToken); @@ -504,14 +505,14 @@ async ValueTask AssertAndLockVersion( /// /// Installs the files for a given BYOND . /// - /// The optional for the operation. + /// The for the operation. /// The being installed with the number set if appropriate. /// Custom zip file to use. Will cause a number to be added. /// If processes should be launched as part of the deployment pipeline. /// The for the operation. /// A representing the running operation. async ValueTask InstallVersionFiles( - JobProgressReporter? progressReporter, + JobProgressReporter progressReporter, EngineVersion version, Stream? customVersionStream, bool deploymentPipelineProcesses, @@ -528,14 +529,12 @@ async ValueTask DirectoryCleanup() try { IEngineInstallationData engineInstallationData; + var remainingProgress = 1.0; if (customVersionStream == null) { - if (progressReporter != null) - progressReporter.StageName = "Downloading version"; - - engineInstallationData = await engineInstaller.DownloadVersion(version, progressReporter, cancellationToken); - - progressReporter?.ReportProgress(null); + using var subReporter = progressReporter.CreateSection("Downloading Version", 0.5); + remainingProgress -= 0.5; + engineInstallationData = await engineInstaller.DownloadVersion(version, subReporter, cancellationToken); } else #pragma warning disable CA2000 // Dispose objects before losing scope, false positive @@ -544,33 +543,45 @@ async ValueTask DirectoryCleanup() customVersionStream); #pragma warning restore CA2000 // Dispose objects before losing scope - await using (engineInstallationData) + JobProgressReporter remainingReporter; + try + { + remainingReporter = progressReporter.CreateSection(null, remainingProgress); + } + catch { - if (progressReporter != null) - progressReporter.StageName = "Cleaning target directory"; + await engineInstallationData.DisposeAsync(); + throw; + } - await directoryCleanupTask; + using (remainingReporter) + { + await using (engineInstallationData) + { + remainingReporter.StageName = "Cleaning target directory"; - if (progressReporter != null) - progressReporter.StageName = "Extracting data"; + await directoryCleanupTask; + remainingReporter.ReportProgress(0.1); + remainingReporter.StageName = "Extracting data"; - logger.LogTrace("Extracting engine to {extractPath}...", installFullPath); - await engineInstallationData.ExtractToPath(installFullPath, cancellationToken); - } + logger.LogTrace("Extracting engine to {extractPath}...", installFullPath); + await engineInstallationData.ExtractToPath(installFullPath, cancellationToken); + remainingReporter.ReportProgress(0.3); + } - if (progressReporter != null) - progressReporter.StageName = "Running installation actions"; + remainingReporter.StageName = "Running installation actions"; - await engineInstaller.Install(version, installFullPath, deploymentPipelineProcesses, cancellationToken); + await engineInstaller.Install(version, installFullPath, deploymentPipelineProcesses, cancellationToken); - if (progressReporter != null) - progressReporter.StageName = "Writing version file"; + remainingReporter.ReportProgress(0.9); + remainingReporter.StageName = "Writing version file"; - // make sure to do this last because this is what tells us we have a valid version in the future - await ioManager.WriteAllBytes( - ioManager.ConcatPath(installFullPath, VersionFileName), - Encoding.UTF8.GetBytes(version.ToString()), - cancellationToken); + // make sure to do this last because this is what tells us we have a valid version in the future + await ioManager.WriteAllBytes( + ioManager.ConcatPath(installFullPath, VersionFileName), + Encoding.UTF8.GetBytes(version.ToString()), + cancellationToken); + } } catch (HttpRequestException ex) { diff --git a/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs index a4ad57a4c02..0df793e219c 100644 --- a/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs @@ -24,10 +24,10 @@ interface IEngineInstaller /// Download a given engine . /// /// The of the engine to download. - /// The optional for the operation. + /// The for the operation. /// The for the operation. /// A resulting in the for the download. - ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken); + ValueTask DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken); /// /// Does actions necessary to get an extracted installation working. diff --git a/src/Tgstation.Server.Host/Components/Engine/IEngineManager.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineManager.cs index 18af1469e6d..7ce4b883db8 100644 --- a/src/Tgstation.Server.Host/Components/Engine/IEngineManager.cs +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineManager.cs @@ -28,14 +28,14 @@ public interface IEngineManager : IComponentService, IDisposable /// /// Change the active . /// - /// The optional for the operation. + /// The for the operation. /// The new . /// Optional of a custom BYOND version zip file. /// If an installation should be performed if the is not installed. If and an installation is required an will be thrown. /// The for the operation. /// A representing the running operation. ValueTask ChangeVersion( - JobProgressReporter? progressReporter, + JobProgressReporter progressReporter, EngineVersion version, Stream? customVersionStream, bool allowInstallation, diff --git a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs index 608d3addb5f..ac4c0964e92 100644 --- a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs @@ -133,23 +133,32 @@ public override IEngineInstallation CreateInstallation(EngineVersion version, st } /// - public override async ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken) + public override async ValueTask DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken) { CheckVersionValidity(version); + ArgumentNullException.ThrowIfNull(jobProgressReporter); // get a lock on a system wide OD repo Logger.LogTrace("Cloning OD repo..."); - var progressSection1 = jobProgressReporter?.CreateSection("Updating OpenDream git repository", 0.5f); - - var repo = await repositoryManager.CloneRepository( - GeneralConfiguration.OpenDreamGitUrl, - null, - null, - null, - progressSection1, - true, - cancellationToken); + var progressSection1 = jobProgressReporter.CreateSection("Updating OpenDream git repository", 0.5f); + IRepository? repo; + try + { + repo = await repositoryManager.CloneRepository( + GeneralConfiguration.OpenDreamGitUrl, + null, + null, + null, + progressSection1, + true, + cancellationToken); + } + catch + { + progressSection1.Dispose(); + throw; + } try { @@ -168,19 +177,23 @@ public override async ValueTask DownloadVersion(EngineV cancellationToken); } - var progressSection2 = jobProgressReporter?.CreateSection("Checking out OpenDream version", 0.5f); + progressSection1.Dispose(); + progressSection1 = null; - var committish = version.SourceSHA - ?? $"{GeneralConfiguration.OpenDreamGitTagPrefix}{version.Version!.Semver()}"; + using (var progressSection2 = jobProgressReporter.CreateSection("Checking out OpenDream version", 0.5f)) + { + var committish = version.SourceSHA + ?? $"{GeneralConfiguration.OpenDreamGitTagPrefix}{version.Version!.Semver()}"; - await repo.CheckoutObject( - committish, - null, - null, - true, - false, - progressSection2, - cancellationToken); + await repo.CheckoutObject( + committish, + null, + null, + true, + false, + progressSection2, + cancellationToken); + } if (!await repo.CommittishIsParent("tgs-min-compat", cancellationToken)) throw new JobException(ErrorCode.OpenDreamTooOld); @@ -192,6 +205,10 @@ await repo.CheckoutObject( repo?.Dispose(); throw; } + finally + { + progressSection1?.Dispose(); + } } /// diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs index e736c4873e1..74417818f19 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs @@ -48,7 +48,7 @@ public interface IRepository : IGitRemoteAdditionalInformation, IDisposable /// The optional password used for fetching from submodule repositories. /// If a submodule update should be attempted after the merge. /// If a hard reset to the target committish should be performed instead of a checkout. - /// The optional to report progress of the operation. + /// The to report progress of the operation. /// The for the operation. /// A representing the running operation. ValueTask CheckoutObject( @@ -57,7 +57,7 @@ ValueTask CheckoutObject( string? password, bool updateSubmodules, bool moveCurrentReference, - JobProgressReporter? progressReporter, + JobProgressReporter progressReporter, CancellationToken cancellationToken); /// @@ -85,14 +85,14 @@ ValueTask AddTestMerge( /// /// Fetch commits from the origin repository. /// - /// The optional to report progress of the operation. + /// The to report progress of the operation. /// The optional username to fetch from the origin repository. /// The optional password to fetch from the origin repository. /// If any events created should be marked as part of the deployment pipeline. /// The for the operation. /// A representing the running operation. ValueTask FetchOrigin( - JobProgressReporter? progressReporter, + JobProgressReporter progressReporter, string? username, string? password, bool deploymentPipeline, diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs index 596301611ac..87cbe63d95a 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs @@ -35,7 +35,7 @@ public interface IRepositoryManager : IDisposable /// The optional branch to clone. /// The optional username to clone from . /// The optional password to clone from . - /// The optional for progress of the clone. + /// The for progress of the clone. /// If submodules should be recusively cloned and initialized. /// The for the operation. /// A resulting i the newly cloned , if one already exists. @@ -44,7 +44,7 @@ public interface IRepositoryManager : IDisposable string? initialBranch, string? username, string? password, - JobProgressReporter? progressReporter, + JobProgressReporter progressReporter, bool recurseSubmodules, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index 6873d429578..9a71bcdcebf 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -232,13 +232,14 @@ await Task.Factory.StartNew( logger.LogTrace("Fetching refspec {refSpec}...", refSpec); var remote = libGitRepo.Network.Remotes.First(); + using var fetchReporter = progressReporter.CreateSection($"Fetch {refSpec}", progressFactor); commands.Fetch( libGitRepo, refSpecList, remote, new FetchOptions().Hydrate( logger, - progressReporter.CreateSection($"Fetch {refSpec}", progressFactor), + fetchReporter, credentialsProvider.GenerateCredentialsHandler(username, password), cancellationToken), logMessage); @@ -267,14 +268,14 @@ await Task.Factory.StartNew( logger.LogTrace("Merging {targetCommitSha} into {currentReference}...", testMergeParameters.TargetCommitSha[..7], Reference); + using var mergeReporter = progressReporter.CreateSection($"Merge {testMergeParameters.TargetCommitSha[..7]}", progressFactor); result = libGitRepo.Merge(testMergeParameters.TargetCommitSha, sig, new MergeOptions { CommitOnSuccess = commitMessage == null, FailOnConflict = false, // Needed to get conflicting files FastForwardStrategy = FastForwardStrategy.NoFastForward, SkipReuc = true, - OnCheckoutProgress = CheckoutProgressHandler( - progressReporter.CreateSection($"Merge {testMergeParameters.TargetCommitSha[..7]}", progressFactor)), + OnCheckoutProgress = CheckoutProgressHandler(mergeReporter), }); } finally @@ -295,7 +296,8 @@ await Task.Factory.StartNew( var revertTo = originalCommit.CanonicalName ?? originalCommit.Tip.Sha; logger.LogDebug("Merge conflict, aborting and reverting to {revertTarget}", revertTo); progressReporter.ReportProgress(0); - RawCheckout(revertTo, false, progressReporter.CreateSection("Hard Reset to {revertTo}", 1.0), cancellationToken); + using var revertReporter = progressReporter.CreateSection("Hard Reset to {revertTo}", 1.0); + RawCheckout(revertTo, false, revertReporter, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); } @@ -343,8 +345,9 @@ await Task.Factory.StartNew( if (updateSubmodules) { + using var progressReporter2 = progressReporter.CreateSection("Update Submodules", progressFactor); await UpdateSubmodules( - progressReporter.CreateSection("Update Submodules", progressFactor), + progressReporter2, username, password, false, @@ -377,7 +380,7 @@ public async ValueTask CheckoutObject( string? password, bool updateSubmodules, bool moveCurrentReference, - JobProgressReporter? progressReporter, + JobProgressReporter progressReporter, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(committish); @@ -388,10 +391,11 @@ await Task.Factory.StartNew( () => { libGitRepo.RemoveUntrackedFiles(); + using var progressReporter3 = progressReporter.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0); RawCheckout( committish, moveCurrentReference, - progressReporter?.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0), + progressReporter3, cancellationToken); }, cancellationToken, @@ -399,17 +403,20 @@ await Task.Factory.StartNew( TaskScheduler.Current); if (updateSubmodules) + { + using var progressReporter2 = progressReporter.CreateSection(null, 1.0 / 3); await UpdateSubmodules( - progressReporter?.CreateSection(null, 1.0 / 3), + progressReporter2, username, password, false, cancellationToken); + } } /// public async ValueTask FetchOrigin( - JobProgressReporter? progressReporter, + JobProgressReporter progressReporter, string? username, string? password, bool deploymentPipeline, @@ -423,13 +430,14 @@ await Task.Factory.StartNew( var remote = libGitRepo.Network.Remotes.First(); try { + using var subReporter = progressReporter.CreateSection("Fetch Origin", 1.0); var fetchOptions = new FetchOptions { Prune = true, TagFetchMode = TagFetchMode.All, }.Hydrate( logger, - progressReporter?.CreateSection("Fetch Origin", 1.0), + subReporter, credentialsProvider.GenerateCredentialsHandler(username, password), cancellationToken); @@ -471,18 +479,23 @@ public async ValueTask ResetToOrigin( logger.LogTrace("Reset to origin..."); var trackedBranch = libGitRepo.Head.TrackedBranch; await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List { trackedBranch.FriendlyName, trackedBranch.Tip.Sha }, deploymentPipeline, cancellationToken); - await ResetToSha( - trackedBranch.Tip.Sha, - progressReporter.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0), - cancellationToken); + + using (var progressReporter2 = progressReporter.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0)) + await ResetToSha( + trackedBranch.Tip.Sha, + progressReporter2, + cancellationToken); if (updateSubmodules) + { + using var progressReporter3 = progressReporter.CreateSection(null, 1.0 / 3); await UpdateSubmodules( - progressReporter.CreateSection(null, 1.0 / 3), + progressReporter3, username, password, deploymentPipeline, cancellationToken); + } } /// @@ -684,9 +697,10 @@ await eventConsumer.HandleEvent( await Task.Factory.StartNew( () => { + using var resetProgress = progressReporter.CreateSection("Hard reset and remove untracked files", 0.1); libGitRepo.Reset(ResetMode.Hard, libGitRepo.Head.Tip, new CheckoutOptions { - OnCheckoutProgress = CheckoutProgressHandler(progressReporter.CreateSection("Hard reset and remove untracked files", 0.1)), + OnCheckoutProgress = CheckoutProgressHandler(resetProgress), }); cancellationToken.ThrowIfCancellationRequested(); libGitRepo.RemoveUntrackedFiles(); @@ -699,10 +713,11 @@ await Task.Factory.StartNew( var remainingProgressFactor = 0.9; if (!synchronizeTrackedBranch) { + using var progressReporter2 = progressReporter.CreateSection("Push to temporary branch", remainingProgressFactor); await PushHeadToTemporaryBranch( username, password, - progressReporter.CreateSection("Push to temporary branch", remainingProgressFactor), + progressReporter2, cancellationToken); return false; } @@ -722,13 +737,24 @@ await PushHeadToTemporaryBranch( var remote = libGitRepo.Network.Remotes.First(); try { - libGitRepo.Network.Push( - libGitRepo.Head, - GeneratePushOptions( - progressReporter.CreateSection("Push to origin", remainingProgressFactor), - username, - password, - cancellationToken)); + using var pushReporter = progressReporter.CreateSection("Push to origin", remainingProgressFactor); + var (pushOptions, progressReporters) = GeneratePushOptions( + pushReporter, + username, + password, + cancellationToken); + try + { + libGitRepo.Network.Push( + libGitRepo.Head, + pushOptions); + } + finally + { + foreach (var progressReporter in progressReporters) + progressReporter.Dispose(); + } + return true; } catch (NonFastForwardException) @@ -882,9 +908,9 @@ protected override void DisposeImpl() /// /// The committish to checkout. /// If a hard reset should actually be performed. - /// The optional for the operation. + /// The for the operation. /// The for the operation. - void RawCheckout(string committish, bool moveCurrentReference, JobProgressReporter? progressReporter, CancellationToken cancellationToken) + void RawCheckout(string committish, bool moveCurrentReference, JobProgressReporter progressReporter, CancellationToken cancellationToken) { logger.LogTrace("Checkout: {committish}", committish); @@ -893,13 +919,10 @@ void RawCheckout(string committish, bool moveCurrentReference, JobProgressReport CheckoutModifiers = CheckoutModifiers.Force, }; - if (progressReporter != null) - { - var stage = $"Checkout {committish}"; - progressReporter = progressReporter.CreateSection(stage, 1.0); - progressReporter.ReportProgress(0); - checkoutOptions.OnCheckoutProgress = CheckoutProgressHandler(progressReporter); - } + var stage = $"Checkout {committish}"; + using var newProgressReporter = progressReporter.CreateSection(stage, 1.0); + newProgressReporter.ReportProgress(0); + checkoutOptions.OnCheckoutProgress = CheckoutProgressHandler(newProgressReporter); cancellationToken.ThrowIfCancellationRequested(); @@ -976,9 +999,38 @@ Task PushHeadToTemporaryBranch(string username, string password, JobProgressRepo try { var forcePushString = String.Format(CultureInfo.InvariantCulture, "+{0}:{0}", branch.CanonicalName); - libGitRepo.Network.Push(remote, forcePushString, GeneratePushOptions(progressReporter.CreateSection(null, 0.9), username, password, cancellationToken)); + + using (var mainPushReporter = progressReporter.CreateSection(null, 0.9)) + { + var (pushOptions, progressReporters) = GeneratePushOptions( + mainPushReporter, + username, + password, + cancellationToken); + + try + { + libGitRepo.Network.Push(remote, forcePushString, pushOptions); + } + finally + { + foreach (var progressReporter in progressReporters) + progressReporter.Dispose(); + } + } + var removalString = String.Format(CultureInfo.InvariantCulture, ":{0}", branch.CanonicalName); - libGitRepo.Network.Push(remote, removalString, GeneratePushOptions(progressReporter.CreateSection(null, 0.1), username, password, cancellationToken)); + using var forcePushReporter = progressReporter.CreateSection(null, 0.1); + var (forcePushOptions, forcePushReporters) = GeneratePushOptions(forcePushReporter, username, password, cancellationToken); + try + { + libGitRepo.Network.Push(remote, removalString, forcePushOptions); + } + finally + { + foreach (var subForcePushReporter in forcePushReporters) + forcePushReporter.Dispose(); + } } catch (UserCancelledException) { @@ -1005,32 +1057,39 @@ Task PushHeadToTemporaryBranch(string username, string password, JobProgressRepo /// The username for the . /// The password for the . /// The for the operation. - /// A new set of . - PushOptions GeneratePushOptions(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken) + /// A new set of and the associated s based off . + (PushOptions PushOptions, IEnumerable SubProgressReporters) GeneratePushOptions(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken) { - var subProgressReporter = progressReporter.CreateSection(null, 0.5); + var packFileCountingReporter = progressReporter.CreateSection(null, 0.25); + var packFileDeltafyingReporter = progressReporter.CreateSection(null, 0.25); + var transferProgressReporter = progressReporter.CreateSection(null, 0.5); - return new PushOptions - { - OnPackBuilderProgress = (stage, current, total) => - { - var baseProgress = stage == PackBuilderStage.Counting ? 0 : 0.5; - var addon = total > 0 && current <= total ? (0.5 * ((double)current / total)) : 0; - progressReporter.ReportProgress(baseProgress + addon); - return !cancellationToken.IsCancellationRequested; - }, - OnNegotiationCompletedBeforePush = (a) => + return ( + PushOptions: new PushOptions { - subProgressReporter = progressReporter.CreateSection(null, 0.5); - return !cancellationToken.IsCancellationRequested; + OnPackBuilderProgress = (stage, current, total) => + { + if (total < current) + total = current; + + var percentage = ((double)current) / total; + (stage == PackBuilderStage.Counting ? packFileCountingReporter : packFileDeltafyingReporter).ReportProgress(percentage); + return !cancellationToken.IsCancellationRequested; + }, + OnNegotiationCompletedBeforePush = (a) => !cancellationToken.IsCancellationRequested, + OnPushTransferProgress = (a, sentBytes, totalBytes) => + { + packFileCountingReporter.ReportProgress((double)sentBytes / totalBytes); + return !cancellationToken.IsCancellationRequested; + }, + CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password), }, - OnPushTransferProgress = (a, sentBytes, totalBytes) => + SubProgressReporters: new List { - progressReporter.ReportProgress((double)sentBytes / totalBytes); - return !cancellationToken.IsCancellationRequested; - }, - CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password), - }; + packFileCountingReporter, + packFileDeltafyingReporter, + transferProgressReporter, + }); } /// @@ -1054,7 +1113,7 @@ string GetRepositoryPath() /// The for the operation. /// A representing the running operation. ValueTask UpdateSubmodules( - JobProgressReporter? progressReporter, + JobProgressReporter progressReporter, string? username, string? password, bool deploymentPipeline, @@ -1062,7 +1121,7 @@ ValueTask UpdateSubmodules( { logger.LogTrace("Updating submodules {withOrWithout} credentials...", username == null ? "without" : "with"); - async ValueTask RecursiveUpdateSubmodules(LibGit2Sharp.IRepository parentRepository, JobProgressReporter? currentProgressReporter, string parentGitDirectory) + async ValueTask RecursiveUpdateSubmodules(LibGit2Sharp.IRepository parentRepository, JobProgressReporter currentProgressReporter, string parentGitDirectory) { var submoduleCount = libGitRepo.Submodules.Count(); if (submoduleCount == 0) @@ -1081,15 +1140,16 @@ async ValueTask RecursiveUpdateSubmodules(LibGit2Sharp.IRepository parentReposit OnCheckoutNotify = (_, _) => !cancellationToken.IsCancellationRequested, }; + using var fetchReporter = currentProgressReporter.CreateSection($"Fetch submodule {submodule.Name}", factor); + submoduleUpdateOptions.FetchOptions.Hydrate( logger, - currentProgressReporter?.CreateSection($"Fetch submodule {submodule.Name}", factor), + fetchReporter, credentialsProvider.GenerateCredentialsHandler(username, password), cancellationToken); - if (currentProgressReporter != null) - submoduleUpdateOptions.OnCheckoutProgress = CheckoutProgressHandler( - currentProgressReporter.CreateSection($"Checkout submodule {submodule.Name}", factor)); + using var checkoutReporter = currentProgressReporter.CreateSection($"Checkout submodule {submodule.Name}", factor); + submoduleUpdateOptions.OnCheckoutProgress = CheckoutProgressHandler(checkoutReporter); logger.LogDebug("Updating submodule {submoduleName}...", submodule.Name); Task RawSubModuleUpdate() => Task.Factory.StartNew( @@ -1106,7 +1166,7 @@ Task RawSubModuleUpdate() => Task.Factory.StartNew( { // workaround for https://github.com/libgit2/libgit2/issues/3820 // kill off the modules/ folder in .git and try again - currentProgressReporter?.ReportProgress(null); + currentProgressReporter.ReportProgress(0); credentialsProvider.CheckBadCredentialsException(ex); logger.LogWarning(ex, "Initial update of submodule {submoduleName} failed. Deleting submodule directories and re-attempting...", submodule.Name); @@ -1145,9 +1205,11 @@ await eventConsumer.HandleEvent( using var submoduleRepo = await submoduleFactory.CreateFromPath( submodulePath, cancellationToken); + + using var submoduleReporter = currentProgressReporter.CreateSection($"Entering submodule \"{submodule.Name}\"...", factor); await RecursiveUpdateSubmodules( submoduleRepo, - currentProgressReporter?.CreateSection($"Entering submodule \"{submodule.Name}\"...", factor), + submoduleReporter, submodulePath); } } diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index 94ae1769510..74a6bc67514 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -123,7 +123,7 @@ public void Dispose() string? initialBranch, string? username, string? password, - JobProgressReporter? progressReporter, + JobProgressReporter progressReporter, bool recurseSubmodules, CancellationToken cancellationToken) { @@ -146,8 +146,8 @@ public void Dispose() if (!await ioManager.DirectoryExists(repositoryPath, cancellationToken)) try { - var cloneProgressReporter = progressReporter?.CreateSection(null, 0.75f); - var checkoutProgressReporter = progressReporter?.CreateSection(null, 0.25f); + using var cloneProgressReporter = progressReporter.CreateSection(null, 0.75f); + using var checkoutProgressReporter = progressReporter.CreateSection(null, 0.25f); var cloneOptions = new CloneOptions { RecurseSubmodules = recurseSubmodules, diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs index 9861a854183..17e820433a7 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs @@ -173,10 +173,7 @@ public async ValueTask RepositoryUpdateJob( var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1)); var progressFactor = 1.0 / numSteps; - JobProgressReporter NextProgressReporter(string? stage) - { - return progressReporter.CreateSection(stage, progressFactor); - } + JobProgressReporter NextProgressReporter(string? stage) => progressReporter.CreateSection(stage, progressFactor); progressReporter.ReportProgress(0); @@ -246,29 +243,35 @@ ValueTask CallLoadRevInfo(Models.TestMerge? testMergeToAdd = null, string? lastO { if (!repo.Tracking) throw new JobException(ErrorCode.RepoReferenceRequired); - await repo.FetchOrigin( - NextProgressReporter("Fetch Origin"), - currentModel.AccessUser, - currentModel.AccessToken, - false, - cancellationToken); + using (var fetchReporter = NextProgressReporter("Fetch Origin")) + await repo.FetchOrigin( + fetchReporter, + currentModel.AccessUser, + currentModel.AccessToken, + false, + cancellationToken); if (!modelHasShaOrReference) { - var fastForward = await repo.MergeOrigin( - NextProgressReporter("Merge Origin"), - committerName, - currentModel.CommitterEmail!, - false, - cancellationToken); + bool? fastForward; + using (var mergeReporter = NextProgressReporter("Merge Origin")) + fastForward = await repo.MergeOrigin( + mergeReporter, + committerName, + currentModel.CommitterEmail!, + false, + cancellationToken); + if (!fastForward.HasValue) throw new JobException(ErrorCode.RepoMergeConflict); + lastRevisionInfo!.OriginCommitSha = await repo.GetOriginSha(cancellationToken); await UpdateRevInfo(); if (fastForward.Value) { + using var syncReporter = NextProgressReporter("Sychronize"); await repo.Synchronize( - NextProgressReporter("Sychronize"), + syncReporter, currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName!, @@ -279,7 +282,7 @@ await repo.Synchronize( postUpdateSha = repo.Head; } else - NextProgressReporter(null).ReportProgress(1.0); + NextProgressReporter(null).Dispose(); } } @@ -303,39 +306,44 @@ await repo.Synchronize( if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null)) throw new JobException(ErrorCode.RepoSwappedShaOrReference); - await repo.CheckoutObject( - committish, - currentModel.AccessUser, - currentModel.AccessToken, - updateSubmodules, - false, - NextProgressReporter("Checkout"), - cancellationToken); + using (var checkoutReporter = NextProgressReporter("Checkout")) + await repo.CheckoutObject( + committish, + currentModel.AccessUser, + currentModel.AccessToken, + updateSubmodules, + false, + checkoutReporter, + cancellationToken); await CallLoadRevInfo(); // we've either seen origin before or what we're checking out is on origin } else - NextProgressReporter(null).ReportProgress(1.0); + NextProgressReporter(null).Dispose(); if (hardResettingToOriginReference) { if (!repo.Tracking) throw new JobException(ErrorCode.RepoReferenceNotTracking); - await repo.ResetToOrigin( - NextProgressReporter("Reset to Origin"), - currentModel.AccessUser, - currentModel.AccessToken, - updateSubmodules, - false, - cancellationToken); - await repo.Synchronize( - NextProgressReporter("Synchronize"), - currentModel.AccessUser, - currentModel.AccessToken, - currentModel.CommitterName!, - currentModel.CommitterEmail!, - true, - false, - cancellationToken); + using (var resetReporter = NextProgressReporter("Reset to Origin")) + await repo.ResetToOrigin( + resetReporter, + currentModel.AccessUser, + currentModel.AccessToken, + updateSubmodules, + false, + cancellationToken); + + using (var syncReporter = NextProgressReporter("Synchronize")) + await repo.Synchronize( + syncReporter, + currentModel.AccessUser, + currentModel.AccessToken, + currentModel.CommitterName!, + currentModel.CommitterEmail!, + true, + false, + cancellationToken); + await CallLoadRevInfo(); // repo head is on origin so force this @@ -486,7 +494,8 @@ await databaseContextFactory.UseContext( // goteem var commitSha = revInfoWereLookingFor.CommitSha!; logger.LogDebug("Reusing existing SHA {sha}...", commitSha); - await repo.ResetToSha(commitSha, NextProgressReporter($"Reset to {commitSha[..7]}"), cancellationToken); + using var resetReporter = NextProgressReporter($"Reset to {commitSha[..7]}"); + await repo.ResetToSha(commitSha, resetReporter, cancellationToken); lastRevisionInfo = revInfoWereLookingFor; } @@ -499,15 +508,17 @@ await databaseContextFactory.UseContext( var fullTestMergeTask = repo.GetTestMerge(newTestMerge, currentModel, cancellationToken); - var mergeResult = await repo.AddTestMerge( - newTestMerge, - committerName, - currentModel.CommitterEmail!, - currentModel.AccessUser, - currentModel.AccessToken, - updateSubmodules, - NextProgressReporter($"Test merge #{newTestMerge.Number}"), - cancellationToken); + TestMergeResult mergeResult; + using (var testMergeReporter = NextProgressReporter($"Test merge #{newTestMerge.Number}")) + mergeResult = await repo.AddTestMerge( + newTestMerge, + committerName, + currentModel.CommitterEmail!, + currentModel.AccessUser, + currentModel.AccessToken, + updateSubmodules, + testMergeReporter, + cancellationToken); if (mergeResult.Status == MergeStatus.Conflicts) throw new JobException( @@ -546,15 +557,17 @@ await databaseContextFactory.UseContext( var currentHead = repo.Head; if (currentModel.PushTestMergeCommits!.Value && (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead))) { - await repo.Synchronize( - NextProgressReporter("Synchronize"), - currentModel.AccessUser, - currentModel.AccessToken, - currentModel.CommitterName!, - currentModel.CommitterEmail!, - false, - false, - cancellationToken); + using (var syncReporter = NextProgressReporter("Synchronize")) + await repo.Synchronize( + syncReporter, + currentModel.AccessUser, + currentModel.AccessToken, + currentModel.CommitterName!, + currentModel.CommitterEmail!, + false, + false, + cancellationToken); + await UpdateRevInfo(); } } @@ -568,17 +581,19 @@ await repo.Synchronize( var secondStep = startReference != null && repo.Head != startSha; // DCTx2: Cancellation token is for job, operations should always run - await repo.CheckoutObject( - startReference ?? startSha, - currentModel.AccessUser, - currentModel.AccessToken, - true, - false, - progressReporter.CreateSection($"Checkout {startReference ?? startSha[..7]}", secondStep ? 0.5 : 1.0), - default); + using (var checkoutReporter = progressReporter.CreateSection($"Checkout {startReference ?? startSha[..7]}", secondStep ? 0.5 : 1.0)) + await repo.CheckoutObject( + startReference ?? startSha, + currentModel.AccessUser, + currentModel.AccessToken, + true, + false, + checkoutReporter, + default); if (secondStep) - await repo.ResetToSha(startSha, progressReporter.CreateSection($"Hard reset to SHA {startSha[..7]}", 0.5), default); + using (var resetReporter = progressReporter.CreateSection($"Hard reset to SHA {startSha[..7]}", 0.5)) + await repo.ResetToSha(startSha, resetReporter, default); throw; } @@ -608,32 +623,35 @@ public async ValueTask RepositoryRecloneJob( string? oldReference; string oldSha; ValueTask deleteTask; - using (var oldRepo = await instance.RepositoryManager.LoadRepository(cancellationToken)) + using (var deleteReporter = progressReporter.CreateSection("Deleting Old Repository", 0.1)) { - if (oldRepo == null) - throw new JobException(ErrorCode.RepoMissing); + using (var oldRepo = await instance.RepositoryManager.LoadRepository(cancellationToken)) + { + if (oldRepo == null) + throw new JobException(ErrorCode.RepoMissing); + + origin = oldRepo.Origin; + oldSha = oldRepo.Head; + oldReference = oldRepo.Reference; + if (oldReference == Repository.NoReference) + oldReference = null; - origin = oldRepo.Origin; - oldSha = oldRepo.Head; - oldReference = oldRepo.Reference; - if (oldReference == Repository.NoReference) - oldReference = null; + deleteTask = instance.RepositoryManager.DeleteRepository(cancellationToken); + } - progressReporter.StageName = "Deleting Old Repository"; - deleteTask = instance.RepositoryManager.DeleteRepository(cancellationToken); + await deleteTask; } - await deleteTask; - progressReporter.ReportProgress(0.1); IRepository newRepo; try { + using var cloneReporter = progressReporter.CreateSection("Cloning New Repository", 0.8); newRepo = await instance.RepositoryManager.CloneRepository( origin, oldReference, currentModel.AccessUser, currentModel.AccessToken, - progressReporter.CreateSection("Cloning New Repository", 0.8), + cloneReporter, true, // TODO: Make configurable maybe... cancellationToken) ?? throw new JobException("A race condition occurred while recloning the repository. Somehow, it was fully cloned instantly after being deleted!"); // I'll take lines of code that should never be hit for $10k @@ -655,14 +673,17 @@ await databaseContextFactory.UseContextTaskReturn(context => } using (newRepo) + using (var checkoutReporter = progressReporter.CreateSection("Checking out previous Detached Commit", 0.1)) + { await newRepo.CheckoutObject( oldSha, currentModel.AccessUser, currentModel.AccessToken, false, oldReference != null, - progressReporter.CreateSection("Checking out previous Detached Commit", 0.1), + checkoutReporter, cancellationToken); + } } } } diff --git a/src/Tgstation.Server.Host/Controllers/EngineController.cs b/src/Tgstation.Server.Host/Controllers/EngineController.cs index c1a20fe99ca..83a113e3b46 100644 --- a/src/Tgstation.Server.Host/Controllers/EngineController.cs +++ b/src/Tgstation.Server.Host/Controllers/EngineController.cs @@ -184,7 +184,8 @@ public async ValueTask Update([FromBody] EngineVersionRequest mod try { - await byondManager.ChangeVersion(null, model.EngineVersion, null, false, cancellationToken); + using var progressReporter = new JobProgressReporter(); + await byondManager.ChangeVersion(progressReporter, model.EngineVersion, null, false, cancellationToken); } catch (InvalidOperationException ex) { diff --git a/src/Tgstation.Server.Host/Extensions/FetchOptionsExtensions.cs b/src/Tgstation.Server.Host/Extensions/FetchOptionsExtensions.cs index 6d71a68edf8..b5e996c311c 100644 --- a/src/Tgstation.Server.Host/Extensions/FetchOptionsExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/FetchOptionsExtensions.cs @@ -20,14 +20,14 @@ static class FetchOptionsExtensions /// /// The to hydrate. /// The for the operation. - /// The optional . + /// The . /// The optional . /// The for the operation. /// The hydrated . public static FetchOptions Hydrate( this FetchOptions fetchOptions, ILogger logger, - JobProgressReporter? progressReporter, + JobProgressReporter progressReporter, CredentialsHandler credentialsHandler, CancellationToken cancellationToken) { @@ -60,10 +60,10 @@ public static FetchOptions Hydrate( /// Generate a from a given and . /// /// The for the operation. - /// The optional of the operation. + /// The of the operation. /// The for the operation. /// A new based on . - static TransferProgressHandler TransferProgressHandler(ILogger logger, JobProgressReporter? progressReporter, CancellationToken cancellationToken) => transferProgress => + static TransferProgressHandler TransferProgressHandler(ILogger logger, JobProgressReporter progressReporter, CancellationToken cancellationToken) => transferProgress => { double? percentage; var totalObjectsToProcess = transferProgress.TotalObjects * 2; diff --git a/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs b/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs index 753f8cf519e..776a2c19e79 100644 --- a/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs +++ b/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs @@ -1,6 +1,7 @@ using System; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Tgstation.Server.Host.Models; @@ -9,7 +10,7 @@ namespace Tgstation.Server.Host.Jobs /// /// Progress reporter for a . /// - public sealed class JobProgressReporter + public sealed class JobProgressReporter : IDisposable { /// /// The name of the current stage. @@ -52,6 +53,24 @@ public string? StageName /// double sectionProgression; + /// + /// The total progress reserved for use in this section. + /// + double? sectionReservations; + + /// + /// Initializes a new instance of the class. + /// + /// This variant has no function. + public JobProgressReporter() + : this( + NullLogger.Instance, + null, + (_, _) => { }, + false) + { + } + /// /// Initializes a new instance of the class. /// @@ -59,27 +78,84 @@ public string? StageName /// The value of . /// The value of . public JobProgressReporter(ILogger logger, string? stageName, Action callback) + : this( + logger, + stageName, + callback, + true) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + /// If an initial call to will be made with only the . + private JobProgressReporter(ILogger logger, string? stageName, Action callback, bool setStageName) { this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.callback = callback ?? throw new ArgumentNullException(nameof(callback)); - StageName = stageName; + if (setStageName) + { + StageName = stageName; + } + else + { + this.stageName = stageName; + } logger.LogDebug("Job progress reporter created. Stage: {stageName}", stageName ?? "(null)"); } + /// + public void Dispose() + { + if (sectionReservations.HasValue) + if (sectionReservations.Value != 1.0) + { + // not an error, processes can throw + sectionReservations = null; + } + else if (sectionProgression < 1.0) + { + logger.LogError( + new InvalidOperationException($"Parent progress reporter has child sections that didn't complete! Current: {sectionProgression}"), + "TGS BUG: Progress reporter children didn't complete!"); + sectionReservations = null; + } + + if (!sectionReservations.HasValue) + ReportProgress(1); + } + /// /// Report progress. /// /// A percentage value from 0.0f-1.0f. public void ReportProgress(double? progress) { + if (sectionReservations.HasValue) + if (progress == 0) + { + // might be a stage reset + sectionReservations = null; + } + else + { + logger.LogError( + new InvalidOperationException("Progress reporter is reporting progress with existing nested sections!"), + "TGS BUG: A progress reporter is using mixed local and nested progress, this is not supported"); + } + var clampedProgress = progress; if (progress.HasValue) if (progress > 1 || progress < 0) { logger.LogError( new ArgumentOutOfRangeException(nameof(progress), progress, "Progress must be a value from 0-1!"), - "Invalid progress value for stage {stageName}", + "TGS BUG: Invalid progress value for stage {stageName}", StageName ?? "(null)"); clampedProgress = null; } @@ -103,16 +179,27 @@ public JobProgressReporter CreateSection(string? newStageName, double percentage { logger.LogError( new ArgumentOutOfRangeException(nameof(percentage), percentage, "Percentage must be a value from 0-1!"), - "Invalid percentage value for stage {newStageName}! Clamping...", + "TGS BUG: Invalid percentage value for stage {newStageName}! Clamping...", newStageName ?? "(null)"); percentage = Math.Min(Math.Max(percentage, 0.0), 1.0); } - var childBaseProgress = sectionProgression; - if (percentage + childBaseProgress > 1.0) + if (!sectionReservations.HasValue) { - var remainingPercentage = 1.0 - childBaseProgress; + if (sectionProgression != 0) + { + logger.LogError( + new InvalidOperationException("Progress reporter is creating a section with local progress!"), + "TGS BUG: A progress reporter is using mixed local and nested progress, this is not supported"); + } + + sectionReservations = 0; + } + + if (percentage + sectionReservations.Value > 1.0001) // floating point >.< + { + var remainingPercentage = 1.0 - sectionReservations.Value; logger.LogError( "Stage {newStageName} is overbudgeted ({budget}/{remainingPercentage})! Clamping...", newStageName, @@ -121,6 +208,9 @@ public JobProgressReporter CreateSection(string? newStageName, double percentage percentage = remainingPercentage; } + Math.Min(sectionReservations.Value + percentage, 1); + + var childLocalProgress = 0.0; var newReporter = new JobProgressReporter( logger, newStageName, @@ -133,11 +223,17 @@ public JobProgressReporter CreateSection(string? newStageName, double percentage return; } - var childLocalProgress = progress.Value * percentage; + var progressWithoutChild = sectionProgression - childLocalProgress; + childLocalProgress = progress.Value * percentage; + + // floating point >.< + sectionProgression = Math.Min(progressWithoutChild + childLocalProgress, 1); + if (sectionProgression > 9.9999) + sectionProgression = 1; - sectionProgression = childLocalProgress + childBaseProgress; callback(currentStage, sectionProgression); - }); + }, + false); newReporter.ReportProgress(0); return newReporter; diff --git a/src/Tgstation.Server.Host/Jobs/JobService.cs b/src/Tgstation.Server.Host/Jobs/JobService.cs index e9967f5189c..d78cc7a2872 100644 --- a/src/Tgstation.Server.Host/Jobs/JobService.cs +++ b/src/Tgstation.Server.Host/Jobs/JobService.cs @@ -460,14 +460,16 @@ void UpdateProgress(string? stage, double? progress) QueueHubUpdate(job.ToApi(), false); logger.LogTrace("Starting job..."); + using var progressReporter = new JobProgressReporter( + loggerFactory.CreateLogger(), + null, + UpdateProgress); + using var innerReporter = progressReporter.CreateSection(null, 1.0); await operation( instanceCoreProvider.GetInstance(job.Instance!), databaseContextFactory, job, - new JobProgressReporter( - loggerFactory.CreateLogger(), - null, - UpdateProgress), + innerReporter, cancellationToken); logger.LogDebug("Job {jobId} completed!", job.Id); diff --git a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestOpenDreamInstaller.cs b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestOpenDreamInstaller.cs index 2a62fea29ad..8950acc6ab6 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestOpenDreamInstaller.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestOpenDreamInstaller.cs @@ -13,6 +13,7 @@ using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.System; using Tgstation.Server.Host.Utils; @@ -52,7 +53,7 @@ static async Task RepoDownloadTest(bool needsClone) null, null, null, - null, + It.IsNotNull(), true, It.IsAny())) .Callback(() => ++cloneAttempts) @@ -85,7 +86,7 @@ static async Task RepoDownloadTest(bool needsClone) Engine = EngineType.OpenDream, SourceSHA = new string('a', Limits.MaximumCommitShaLength), }, - null, + new JobProgressReporter(), CancellationToken.None); diff --git a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryManager.cs b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryManager.cs index c4a17f51401..2cdd59d19b8 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryManager.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryManager.cs @@ -15,6 +15,7 @@ using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Jobs; namespace Tgstation.Server.Host.Components.Repository.Tests { @@ -86,7 +87,7 @@ public async Task TestBasicClone() null, null, null, - null, + new JobProgressReporter(), false, CancellationToken.None); diff --git a/tests/Tgstation.Server.Host.Tests/Jobs/TestJobProgressReporter.cs b/tests/Tgstation.Server.Host.Tests/Jobs/TestJobProgressReporter.cs new file mode 100644 index 00000000000..f6d6cd9348f --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/Jobs/TestJobProgressReporter.cs @@ -0,0 +1,66 @@ +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Moq; + +namespace Tgstation.Server.Host.Jobs.Tests +{ + [TestClass] + public sealed class TestJobProgressReporter + { + string expectedStageName = null; + double? expectedProgress = null; + void Validate(string stageName, double? progress) + { + Assert.AreEqual(expectedStageName, stageName); + Assert.AreEqual(expectedProgress, progress); + } + + JobProgressReporter Setup() + { + expectedStageName = null; + expectedProgress = 0; + return new JobProgressReporter( + Mock.Of>(), + null, + Validate); + } + + [TestMethod] + public void TestBasicUsage() + { + var progressReporter = Setup(); + + expectedProgress = 0.4; + progressReporter.ReportProgress(0.4); + expectedProgress = 1.0; + progressReporter.ReportProgress(1.0); + } + + [TestMethod] + public void TestNestedUsage() + { + var progressReporter = Setup(); + + expectedStageName = "Test1"; + var subReporter1 = progressReporter.CreateSection("Test1", 0.5); + expectedProgress = 0.1; + subReporter1.ReportProgress(0.2); + expectedProgress = 0.4; + subReporter1.ReportProgress(0.8); + + expectedStageName = "Test2"; + var subReporter2 = progressReporter.CreateSection("Test2", 0.5); + + expectedStageName = "Test1"; + expectedProgress = 0.5; + subReporter1.ReportProgress(1); + + expectedStageName = "Test2"; + expectedProgress = 0.6; + subReporter2.ReportProgress(0.2); + expectedProgress = 1.0; + subReporter2.ReportProgress(1); + } + } +} diff --git a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs index 9c2b44c9624..67ed9e2178f 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs @@ -22,6 +22,7 @@ using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.System; using Tgstation.Server.Host.Utils; @@ -142,7 +143,7 @@ public static async ValueTask DownloadEngineVersion( using var windowsByondInstaller = byondInstaller as WindowsByondInstaller; // get the bytes for stable - return await byondInstaller.DownloadVersion(compatVersion, null, cancellationToken); + return await byondInstaller.DownloadVersion(compatVersion, new JobProgressReporter(), cancellationToken); } public async Task RunCompatTests( From 1c3a99ab6a02fa678827590611235cfee09d8b02 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 18 Aug 2024 00:00:14 -0400 Subject: [PATCH 3/4] Fix issues with retrieving process memory --- .../Components/Session/SessionController.cs | 2 +- src/Tgstation.Server.Host/System/IProcessBase.cs | 2 +- src/Tgstation.Server.Host/System/Process.cs | 16 +++++++++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 6880ed7f6d2..754b672b7d6 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -122,7 +122,7 @@ async Task Wrap() public FifoSemaphore TopicSendSemaphore { get; } /// - public long MemoryUsage => process.MemoryUsage; + public long? MemoryUsage => process.MemoryUsage; /// /// The for the . diff --git a/src/Tgstation.Server.Host/System/IProcessBase.cs b/src/Tgstation.Server.Host/System/IProcessBase.cs index f34d5e6597c..6ae88dce502 100644 --- a/src/Tgstation.Server.Host/System/IProcessBase.cs +++ b/src/Tgstation.Server.Host/System/IProcessBase.cs @@ -16,7 +16,7 @@ public interface IProcessBase /// /// Gets the process' memory usage in bytes. /// - long MemoryUsage { get; } + long? MemoryUsage { get; } /// /// Set's the owned to a non-normal value. diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs index 64c92bf89b9..13a1a278f50 100644 --- a/src/Tgstation.Server.Host/System/Process.cs +++ b/src/Tgstation.Server.Host/System/Process.cs @@ -23,7 +23,21 @@ sealed class Process : IProcess public Task Lifetime { get; } /// - public long MemoryUsage => handle.VirtualMemorySize64; + public long? MemoryUsage + { + get + { + try + { + return handle.VirtualMemorySize64; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to get PID {pid}'s memory usage!", Id); + return null; + } + } + } /// /// The for the . From bab11e603aa2d03f0432b437fd10778cce5f624f Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 18 Aug 2024 00:26:59 -0400 Subject: [PATCH 4/4] Fix Release build --- src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs b/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs index 776a2c19e79..12ee246858f 100644 --- a/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs +++ b/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs @@ -197,7 +197,8 @@ public JobProgressReporter CreateSection(string? newStageName, double percentage sectionReservations = 0; } - if (percentage + sectionReservations.Value > 1.0001) // floating point >.< + // floating point >.< + if (percentage + sectionReservations.Value > 1.0001) { var remainingPercentage = 1.0 - sectionReservations.Value; logger.LogError(