Fix issues with JobProgressReporter

This commit is contained in:
Jordan Dominion
2024-08-18 00:00:02 -04:00
parent f0bf09665d
commit df43a072b2
20 changed files with 526 additions and 247 deletions
@@ -146,7 +146,7 @@ namespace Tgstation.Server.Host.Components.Engine
}
/// <inheritdoc />
public override async ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter? progressReporter, CancellationToken cancellationToken)
public override async ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter progressReporter, CancellationToken cancellationToken)
{
CheckVersionValidity(version);
@@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components.Engine
=> DelegateCall(version, installer => installer.CreateInstallation(version, path, installationTask));
/// <inheritdoc />
public ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken)
public ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken)
=> DelegateCall(version, installer => installer.DownloadVersion(version, jobProgressReporter, cancellationToken));
/// <inheritdoc />
@@ -52,7 +52,7 @@ namespace Tgstation.Server.Host.Components.Engine
public abstract ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken);
/// <inheritdoc />
public abstract ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken);
public abstract ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken);
/// <inheritdoc />
public abstract ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken);
@@ -118,7 +118,7 @@ namespace Tgstation.Server.Host.Components.Engine
/// <inheritdoc />
public async ValueTask ChangeVersion(
JobProgressReporter? progressReporter,
JobProgressReporter progressReporter,
EngineVersion version,
Stream? customVersionStream,
bool allowInstallation,
@@ -166,8 +166,11 @@ namespace Tgstation.Server.Host.Components.Engine
"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 @@ namespace Tgstation.Server.Host.Components.Engine
/// <summary>
/// Ensures a BYOND <paramref name="version"/> is installed if it isn't already.
/// </summary>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="version">The <see cref="EngineVersion"/> to install.</param>
/// <param name="customVersionStream">Optional custom zip file <see cref="Stream"/> to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
/// <param name="neededForLock">If this BYOND version is required as part of a locking operation.</param>
@@ -396,7 +399,7 @@ namespace Tgstation.Server.Host.Components.Engine
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="EngineExecutableLock"/>.</returns>
async ValueTask<EngineExecutableLock> AssertAndLockVersion(
JobProgressReporter? progressReporter,
JobProgressReporter progressReporter,
EngineVersion version,
Stream? customVersionStream,
bool neededForLock,
@@ -443,8 +446,7 @@ namespace Tgstation.Server.Host.Components.Engine
{
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 @@ namespace Tgstation.Server.Host.Components.Engine
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<string> { versionString }, deploymentPipelineProcesses, cancellationToken);
@@ -504,14 +505,14 @@ namespace Tgstation.Server.Host.Components.Engine
/// <summary>
/// Installs the files for a given BYOND <paramref name="version"/>.
/// </summary>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="version">The <see cref="EngineVersion"/> being installed with the <see cref="Version.Build"/> number set if appropriate.</param>
/// <param name="customVersionStream">Custom zip file <see cref="Stream"/> to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
/// <param name="deploymentPipelineProcesses">If processes should be launched as part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask InstallVersionFiles(
JobProgressReporter? progressReporter,
JobProgressReporter progressReporter,
EngineVersion version,
Stream? customVersionStream,
bool deploymentPipelineProcesses,
@@ -528,14 +529,12 @@ namespace Tgstation.Server.Host.Components.Engine
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 @@ namespace Tgstation.Server.Host.Components.Engine
customVersionStream);
#pragma warning restore CA2000 // Dispose objects before losing scope
await using (engineInstallationData)
JobProgressReporter remainingReporter;
try
{
if (progressReporter != null)
progressReporter.StageName = "Cleaning target directory";
await directoryCleanupTask;
if (progressReporter != null)
progressReporter.StageName = "Extracting data";
logger.LogTrace("Extracting engine to {extractPath}...", installFullPath);
await engineInstallationData.ExtractToPath(installFullPath, cancellationToken);
remainingReporter = progressReporter.CreateSection(null, remainingProgress);
}
catch
{
await engineInstallationData.DisposeAsync();
throw;
}
if (progressReporter != null)
progressReporter.StageName = "Running installation actions";
using (remainingReporter)
{
await using (engineInstallationData)
{
remainingReporter.StageName = "Cleaning target directory";
await engineInstaller.Install(version, installFullPath, deploymentPipelineProcesses, cancellationToken);
await directoryCleanupTask;
remainingReporter.ReportProgress(0.1);
remainingReporter.StageName = "Extracting data";
if (progressReporter != null)
progressReporter.StageName = "Writing version file";
logger.LogTrace("Extracting engine to {extractPath}...", installFullPath);
await engineInstallationData.ExtractToPath(installFullPath, cancellationToken);
remainingReporter.ReportProgress(0.3);
}
// 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);
remainingReporter.StageName = "Running installation actions";
await engineInstaller.Install(version, installFullPath, deploymentPipelineProcesses, cancellationToken);
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);
}
}
catch (HttpRequestException ex)
{
@@ -24,10 +24,10 @@ namespace Tgstation.Server.Host.Components.Engine
/// Download a given engine <paramref name="version"/>.
/// </summary>
/// <param name="version">The <see cref="EngineVersion"/> of the engine to download.</param>
/// <param name="jobProgressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="jobProgressReporter">The <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IEngineInstallationData"/> for the download.</returns>
ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken);
ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken);
/// <summary>
/// Does actions necessary to get an extracted installation working.
@@ -28,14 +28,14 @@ namespace Tgstation.Server.Host.Components.Engine
/// <summary>
/// Change the active <see cref="EngineVersion"/>.
/// </summary>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="version">The new <see cref="EngineVersion"/>.</param>
/// <param name="customVersionStream">Optional <see cref="Stream"/> of a custom BYOND version zip file.</param>
/// <param name="allowInstallation">If an installation should be performed if the <paramref name="version"/> is not installed. If <see langword="false"/> and an installation is required an <see cref="InvalidOperationException"/> will be thrown.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask ChangeVersion(
JobProgressReporter? progressReporter,
JobProgressReporter progressReporter,
EngineVersion version,
Stream? customVersionStream,
bool allowInstallation,
@@ -133,23 +133,32 @@ namespace Tgstation.Server.Host.Components.Engine
}
/// <inheritdoc />
public override async ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken)
public override async ValueTask<IEngineInstallationData> 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 @@ namespace Tgstation.Server.Host.Components.Engine
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 @@ namespace Tgstation.Server.Host.Components.Engine
repo?.Dispose();
throw;
}
finally
{
progressSection1?.Dispose();
}
}
/// <inheritdoc />
@@ -48,7 +48,7 @@ namespace Tgstation.Server.Host.Components.Repository
/// <param name="password">The optional password used for fetching from submodule repositories.</param>
/// <param name="updateSubmodules">If a submodule update should be attempted after the merge.</param>
/// <param name="moveCurrentReference">If a hard reset to the target committish should be performed instead of a checkout.</param>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask CheckoutObject(
@@ -57,7 +57,7 @@ namespace Tgstation.Server.Host.Components.Repository
string? password,
bool updateSubmodules,
bool moveCurrentReference,
JobProgressReporter? progressReporter,
JobProgressReporter progressReporter,
CancellationToken cancellationToken);
/// <summary>
@@ -85,14 +85,14 @@ namespace Tgstation.Server.Host.Components.Repository
/// <summary>
/// Fetch commits from the origin repository.
/// </summary>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="username">The optional username to fetch from the origin repository.</param>
/// <param name="password">The optional password to fetch from the origin repository.</param>
/// <param name="deploymentPipeline">If any events created should be marked as part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask FetchOrigin(
JobProgressReporter? progressReporter,
JobProgressReporter progressReporter,
string? username,
string? password,
bool deploymentPipeline,
@@ -35,7 +35,7 @@ namespace Tgstation.Server.Host.Components.Repository
/// <param name="initialBranch">The optional branch to clone.</param>
/// <param name="username">The optional username to clone from <paramref name="url"/>.</param>
/// <param name="password">The optional password to clone from <paramref name="url"/>.</param>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for progress of the clone.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for progress of the clone.</param>
/// <param name="recurseSubmodules">If submodules should be recusively cloned and initialized.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting i the newly cloned <see cref="IRepository"/>, <see langword="null"/> if one already exists.</returns>
@@ -44,7 +44,7 @@ namespace Tgstation.Server.Host.Components.Repository
string? initialBranch,
string? username,
string? password,
JobProgressReporter? progressReporter,
JobProgressReporter progressReporter,
bool recurseSubmodules,
CancellationToken cancellationToken);
@@ -232,13 +232,14 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
string? password,
bool updateSubmodules,
bool moveCurrentReference,
JobProgressReporter? progressReporter,
JobProgressReporter progressReporter,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(committish);
@@ -388,10 +391,11 @@ namespace Tgstation.Server.Host.Components.Repository
() =>
{
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 @@ namespace Tgstation.Server.Host.Components.Repository
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);
}
}
/// <inheritdoc />
public async ValueTask FetchOrigin(
JobProgressReporter? progressReporter,
JobProgressReporter progressReporter,
string? username,
string? password,
bool deploymentPipeline,
@@ -423,13 +430,14 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
logger.LogTrace("Reset to origin...");
var trackedBranch = libGitRepo.Head.TrackedBranch;
await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List<string> { 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);
}
}
/// <inheritdoc />
@@ -684,9 +697,10 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
/// </summary>
/// <param name="committish">The committish to checkout.</param>
/// <param name="moveCurrentReference">If a hard reset should actually be performed.</param>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
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 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
/// <param name="username">The username for the <see cref="credentialsProvider"/>.</param>
/// <param name="password">The password for the <see cref="credentialsProvider"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A new set of <see cref="PushOptions"/>.</returns>
PushOptions GeneratePushOptions(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken)
/// <returns>A new set of <see cref="PushOptions"/> and the associated <see cref="JobProgressReporter"/>s based off <paramref name="progressReporter"/>.</returns>
(PushOptions PushOptions, IEnumerable<JobProgressReporter> 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) =>
return (
PushOptions: new PushOptions
{
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;
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),
},
OnNegotiationCompletedBeforePush = (a) =>
SubProgressReporters: new List<JobProgressReporter>
{
subProgressReporter = progressReporter.CreateSection(null, 0.5);
return !cancellationToken.IsCancellationRequested;
},
OnPushTransferProgress = (a, sentBytes, totalBytes) =>
{
progressReporter.ReportProgress((double)sentBytes / totalBytes);
return !cancellationToken.IsCancellationRequested;
},
CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password),
};
packFileCountingReporter,
packFileDeltafyingReporter,
transferProgressReporter,
});
}
/// <summary>
@@ -1054,7 +1113,7 @@ namespace Tgstation.Server.Host.Components.Repository
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask UpdateSubmodules(
JobProgressReporter? progressReporter,
JobProgressReporter progressReporter,
string? username,
string? password,
bool deploymentPipeline,
@@ -1062,7 +1121,7 @@ namespace Tgstation.Server.Host.Components.Repository
{
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 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
{
// 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 @@ namespace Tgstation.Server.Host.Components.Repository
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);
}
}
@@ -123,7 +123,7 @@ namespace Tgstation.Server.Host.Components.Repository
string? initialBranch,
string? username,
string? password,
JobProgressReporter? progressReporter,
JobProgressReporter progressReporter,
bool recurseSubmodules,
CancellationToken cancellationToken)
{
@@ -146,8 +146,8 @@ namespace Tgstation.Server.Host.Components.Repository
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,
@@ -173,10 +173,7 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
{
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 @@ namespace Tgstation.Server.Host.Components.Repository
postUpdateSha = repo.Head;
}
else
NextProgressReporter(null).ReportProgress(1.0);
NextProgressReporter(null).Dispose();
}
}
@@ -303,39 +306,44 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
// 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 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
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 @@ namespace Tgstation.Server.Host.Components.Repository
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;
progressReporter.StageName = "Deleting Old Repository";
deleteTask = instance.RepositoryManager.DeleteRepository(cancellationToken);
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 @@ namespace Tgstation.Server.Host.Components.Repository
}
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);
}
}
}
}
@@ -184,7 +184,8 @@ namespace Tgstation.Server.Host.Controllers
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)
{
@@ -20,14 +20,14 @@ namespace Tgstation.Server.Host.Extensions
/// </summary>
/// <param name="fetchOptions">The <see cref="FetchOptions"/> to hydrate.</param>
/// <param name="logger">The <see cref="ILogger"/> for the operation.</param>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/>.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/>.</param>
/// <param name="credentialsHandler">The optional <see cref="CredentialsHandler"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>The hydrated <paramref name="fetchOptions"/>.</returns>
public static FetchOptions Hydrate(
this FetchOptions fetchOptions,
ILogger logger,
JobProgressReporter? progressReporter,
JobProgressReporter progressReporter,
CredentialsHandler credentialsHandler,
CancellationToken cancellationToken)
{
@@ -60,10 +60,10 @@ namespace Tgstation.Server.Host.Extensions
/// Generate a <see cref="LibGit2Sharp.Handlers.TransferProgressHandler"/> from a given <paramref name="progressReporter"/> and <paramref name="cancellationToken"/>.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> for the operation.</param>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> of the operation.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> of the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A new <see cref="LibGit2Sharp.Handlers.TransferProgressHandler"/> based on <paramref name="progressReporter"/>.</returns>
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;
@@ -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
/// <summary>
/// Progress reporter for a <see cref="Job"/>.
/// </summary>
public sealed class JobProgressReporter
public sealed class JobProgressReporter : IDisposable
{
/// <summary>
/// The name of the current stage.
@@ -52,6 +53,24 @@ namespace Tgstation.Server.Host.Jobs
/// </summary>
double sectionProgression;
/// <summary>
/// The total progress reserved for use in this section.
/// </summary>
double? sectionReservations;
/// <summary>
/// Initializes a new instance of the <see cref="JobProgressReporter"/> class.
/// </summary>
/// <remarks>This variant has no function.</remarks>
public JobProgressReporter()
: this(
NullLogger<JobProgressReporter>.Instance,
null,
(_, _) => { },
false)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="JobProgressReporter"/> class.
/// </summary>
@@ -59,27 +78,84 @@ namespace Tgstation.Server.Host.Jobs
/// <param name="stageName">The value of <see cref="StageName"/>.</param>
/// <param name="callback">The value of <see cref="callback"/>.</param>
public JobProgressReporter(ILogger<JobProgressReporter> logger, string? stageName, Action<string?, double?> callback)
: this(
logger,
stageName,
callback,
true)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="JobProgressReporter"/> class.
/// </summary>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="stageName">The value of <see cref="StageName"/>.</param>
/// <param name="callback">The value of <see cref="callback"/>.</param>
/// <param name="setStageName">If <see langword="true"/> an initial call to <paramref name="callback"/> will be made with only the <paramref name="stageName"/>.</param>
private JobProgressReporter(ILogger<JobProgressReporter> logger, string? stageName, Action<string?, double?> 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)");
}
/// <inheritdoc />
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);
}
/// <summary>
/// Report progress.
/// </summary>
/// <param name="progress">A percentage value from 0.0f-1.0f.</param>
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 @@ namespace Tgstation.Server.Host.Jobs
{
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 @@ namespace Tgstation.Server.Host.Jobs
percentage = remainingPercentage;
}
Math.Min(sectionReservations.Value + percentage, 1);
var childLocalProgress = 0.0;
var newReporter = new JobProgressReporter(
logger,
newStageName,
@@ -133,11 +223,17 @@ namespace Tgstation.Server.Host.Jobs
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;
+6 -4
View File
@@ -460,14 +460,16 @@ namespace Tgstation.Server.Host.Jobs
QueueHubUpdate(job.ToApi(), false);
logger.LogTrace("Starting job...");
using var progressReporter = new JobProgressReporter(
loggerFactory.CreateLogger<JobProgressReporter>(),
null,
UpdateProgress);
using var innerReporter = progressReporter.CreateSection(null, 1.0);
await operation(
instanceCoreProvider.GetInstance(job.Instance!),
databaseContextFactory,
job,
new JobProgressReporter(
loggerFactory.CreateLogger<JobProgressReporter>(),
null,
UpdateProgress),
innerReporter,
cancellationToken);
logger.LogDebug("Job {jobId} completed!", job.Id);
@@ -13,6 +13,7 @@ using Tgstation.Server.Common.Http;
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 @@ namespace Tgstation.Server.Host.Components.Engine.Tests
null,
null,
null,
null,
It.IsNotNull<JobProgressReporter>(),
true,
It.IsAny<CancellationToken>()))
.Callback(() => ++cloneAttempts)
@@ -85,7 +86,7 @@ namespace Tgstation.Server.Host.Components.Engine.Tests
Engine = EngineType.OpenDream,
SourceSHA = new string('a', Limits.MaximumCommitShaLength),
},
null,
new JobProgressReporter(),
CancellationToken.None);
@@ -15,6 +15,7 @@ using Remora.Rest.Core;
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 @@ namespace Tgstation.Server.Host.Components.Repository.Tests
null,
null,
null,
null,
new JobProgressReporter(),
false,
CancellationToken.None);
@@ -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<ILogger<JobProgressReporter>>(),
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);
}
}
}
@@ -22,6 +22,7 @@ using Tgstation.Server.Host.Components.Events;
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 @@ namespace Tgstation.Server.Tests.Live.Instance
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(