diff --git a/build/Version.props b/build/Version.props index 7fbb04ccd2..f8653e54ae 100644 --- a/build/Version.props +++ b/build/Version.props @@ -5,10 +5,10 @@ 5.14.0 4.7.1 - 9.11.0 + 9.11.1 6.0.0 - 11.0.0 - 12.0.0 + 11.0.1 + 12.0.1 6.5.2 5.6.1 1.4.0 diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 1ca26752ee..e10d4577c8 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -486,7 +486,7 @@ namespace Tgstation.Server.Api.Models DreamDaemonPortInUse, /// - /// Failed to post GitHub comments, send chat message, or send TGS event. + /// Failed to post GitHub comments, or send TGS event. /// [Description("The deployment succeeded but one or more notification events failed!")] PostDeployFailure, diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index ecc87450ba..6e9745399e 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -6,7 +6,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; + using Newtonsoft.Json; + using Serilog.Context; using Tgstation.Server.Api.Models.Internal; @@ -354,7 +356,7 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public Action QueueDeploymentMessage( + public Func> QueueDeploymentMessage( Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, @@ -368,7 +370,7 @@ namespace Tgstation.Server.Host.Components.Chat logger.LogTrace("Sending deployment message for RevisionInformation: {revisionInfoId}", revisionInformation.Id); - var callbacks = new List>(); + var callbacks = new List>>>(); var task = Task.WhenAll( wdChannels.Select( @@ -408,17 +410,44 @@ namespace Tgstation.Server.Host.Components.Chat AddMessageTask(task); - async Task CollateTasks(string errorMessage, string dreamMakerOutput) + Task callbackTask = null; + Func finalUpdateAction = null; + async Task CallbackTask(string errorMessage, string dreamMakerOutput) { await task; - await Task.WhenAll( + var callbackResultTasks = callbacks.Select( x => x( errorMessage, - dreamMakerOutput))); + dreamMakerOutput)) + .ToList(); + + await Task.WhenAll(callbackResultTasks); + + finalUpdateAction = active => Task.WhenAll(callbackResultTasks.Select(task => task.Result(active))); } - return (errorMessage, dreamMakerOutput) => AddMessageTask(CollateTasks(errorMessage, dreamMakerOutput)); + async Task CompletionTask(bool active) + { + try + { + await callbackTask; + } + catch + { + // Handled in AddMessageTask + return; + } + + AddMessageTask(finalUpdateAction(active)); + } + + return (errorMessage, dreamMakerOutput) => + { + callbackTask = CallbackTask(errorMessage, dreamMakerOutput); + AddMessageTask(callbackTask); + return active => AddMessageTask(CompletionTask(active)); + }; } /// diff --git a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs index 7004bfc592..f5ef9da42f 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs @@ -66,8 +66,8 @@ namespace Tgstation.Server.Host.Components.Chat /// The repository GitHub owner, if any. /// The repository GitHub name, if any. /// if the local deployment commit was pushed to the remote repository. - /// An to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. - Action QueueDeploymentMessage( + /// A to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. Returns an to call to mark the deployment as active/inactive. Parameter: If the deployment is being activated or inactivated. + Func> QueueDeploymentMessage( Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 622dec0e00..142513c275 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -326,7 +326,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public override async Task> SendUpdateMessage( + public override async Task>>> SendUpdateMessage( Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, @@ -377,13 +377,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers { var completionString = errorMessage == null ? "Succeeded" : "Failed"; - embed = new Embed + Embed CreateUpdatedEmbed(string message, Color color) => new Embed { Author = embed.Author, - Colour = errorMessage == null ? Color.Green : Color.Red, - Description = errorMessage == null - ? "The deployment completed successfully and will be available at the next server reboot." - : "The deployment failed.", + Colour = color, + Description = message, Fields = fields, Title = embed.Title, Footer = new EmbedFooter( @@ -391,6 +389,15 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Timestamp = DateTimeOffset.UtcNow, }; + if (errorMessage == null) + embed = CreateUpdatedEmbed( + "The deployment completed successfully and will be available at the next server reboot.", + Color.Blue); + else + embed = CreateUpdatedEmbed( + "The deployment failed.", + Color.Red); + var showDMOutput = outputDisplayType switch { DiscordDMOutputDisplayType.Always => true, @@ -417,13 +424,14 @@ namespace Tgstation.Server.Host.Components.Chat.Providers errorMessage, false)); - var updatedMessage = $"DM: Deployment {completionString}!"; + var updatedMessageText = $"DM: Deployment {completionString}!"; + IMessage updatedMessage = null; async Task CreateUpdatedMessage() { var createUpdatedMessageResponse = await channelsClient.CreateMessageAsync( new Snowflake(channelId), - updatedMessage, + updatedMessageText, embeds: new List { embed }, ct: cancellationToken); @@ -431,6 +439,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Logger.LogWarning( "Creating updated deploy embed failed: {result}", createUpdatedMessageResponse.LogFormat()); + else + updatedMessage = createUpdatedMessageResponse.Entity; } if (!messageResponse.IsSuccess) @@ -440,7 +450,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers var editResponse = await channelsClient.EditMessageAsync( new Snowflake(channelId), messageResponse.Entity.ID, - updatedMessage, + updatedMessageText, embeds: new List { embed }, ct: cancellationToken); @@ -452,7 +462,37 @@ namespace Tgstation.Server.Host.Components.Chat.Providers editResponse.LogFormat()); await CreateUpdatedMessage(); } + else + updatedMessage = editResponse.Entity; } + + return async (active) => + { + if (updatedMessage == null || errorMessage != null) + return; + + if (active) + embed = CreateUpdatedEmbed( + "The deployment completed successfully and was applied to server.", + Color.Green); + else + embed = CreateUpdatedEmbed( + "This deployment has been superceeded by a new one.", + Color.Gray); + + var editResponse = await channelsClient.EditMessageAsync( + new Snowflake(channelId), + updatedMessage.ID, + updatedMessageText, + embeds: new List { embed }, + ct: cancellationToken); + + if (!editResponse.IsSuccess) + Logger.LogWarning( + "Finalizing deploy embed {messageId} failed: {result}", + messageResponse.Entity.ID, + editResponse.LogFormat()); + }; }; } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs index abf1eefe20..e8d1fdf2a0 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs @@ -90,8 +90,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// The to send to. /// if the local deployment commit was pushed to the remote repository. /// The for the operation. - /// A resulting in a to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. - Task> SendUpdateMessage( + /// A resulting in a to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. Returns another callback which should be called to mark the deployment as active. + Task>>> SendUpdateMessage( RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 2e02dfcedb..c94908a64f 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -218,7 +218,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public override async Task> SendUpdateMessage( + public override async Task>>> SendUpdateMessage( Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, @@ -281,14 +281,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers channelId, cancellationToken); - return (errorMessage, dreamMakerOutput) => SendMessage( - null, - new MessageContent - { - Text = $"DM: Deployment {(errorMessage == null ? "complete" : "failed")}!", - }, - channelId, - cancellationToken); + return async (errorMessage, dreamMakerOutput) => + { + await SendMessage( + null, + new MessageContent + { + Text = $"DM: Deployment {(errorMessage == null ? "complete" : "failed")}!", + }, + channelId, + cancellationToken); + + return active => Task.CompletedTask; + }; } /// diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs index 35840d67db..7a3d9d507d 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs @@ -181,7 +181,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers public abstract Task SendMessage(Message replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken); /// - public abstract Task> SendUpdateMessage( + public abstract Task>>> SendUpdateMessage( RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index d2c48f3b4d..09567ba79c 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -74,16 +74,16 @@ namespace Tgstation.Server.Host.Components.Deployment /// readonly IDictionary jobLockCounts; + /// + /// resulting in the latest yet to exist. + /// + volatile TaskCompletionSource newerDmbTcs; + /// /// representing calls to . /// Task cleanupTask; - /// - /// resulting in the latest yet to exist. - /// - TaskCompletionSource newerDmbTcs; - /// /// The latest . /// @@ -128,7 +128,7 @@ namespace Tgstation.Server.Host.Components.Deployment public void Dispose() => cleanupCts.Dispose(); // we don't dispose nextDmbProvider here, since it might be the only thing we have /// - public async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken) + public async Task LoadCompileJob(CompileJob job, Action activationAction, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(job); @@ -143,8 +143,9 @@ namespace Tgstation.Server.Host.Components.Deployment metadata, job); await remoteDeploymentManager.StageDeployment( - newProvider.CompileJob, - cancellationToken); + newProvider.CompileJob, + activationAction, + cancellationToken); } lock (jobLockCounts) @@ -153,8 +154,7 @@ namespace Tgstation.Server.Host.Components.Deployment nextDmbProvider = newProvider; // Oh god dammit - var temp = newerDmbTcs; - newerDmbTcs = new TaskCompletionSource(); + var temp = Interlocked.Exchange(ref newerDmbTcs, new TaskCompletionSource()); temp.SetResult(); } } @@ -170,7 +170,7 @@ namespace Tgstation.Server.Host.Components.Deployment { var jobId = nextDmbProvider.CompileJob.Id; var incremented = jobLockCounts[jobId.Value] += lockCount; - logger.LogTrace("Compile job {0} lock count now: {1}", jobId, incremented); + logger.LogTrace("Compile job {jobId} lock count now: {lockCount}", jobId, incremented); return nextDmbProvider; } } @@ -189,10 +189,16 @@ namespace Tgstation.Server.Host.Components.Deployment .FirstOrDefaultAsync(cancellationToken); }); - if (cj == default(CompileJob)) - return; - await LoadCompileJob(cj, cancellationToken); - started = true; + try + { + if (cj == default(CompileJob)) + return; + await LoadCompileJob(cj, null, cancellationToken); + } + finally + { + started = true; + } // we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet } @@ -202,6 +208,9 @@ namespace Tgstation.Server.Host.Components.Deployment { try { + lock (jobLockCounts) + remoteDeploymentManagerFactory.ForgetLocalStateForCompileJobs(jobLockCounts.Keys); + using (cancellationToken.Register(() => cleanupCts.Cancel())) await cleanupTask; } @@ -218,7 +227,7 @@ namespace Tgstation.Server.Host.Components.Deployment ArgumentNullException.ThrowIfNull(compileJob); // ensure we have the entire metadata tree - logger.LogTrace("Loading compile job {0}...", compileJob.Id); + logger.LogTrace("Loading compile job {id}...", compileJob.Id); await databaseContextFactory.UseContext( async db => compileJob = await db .CompileJobs @@ -240,7 +249,7 @@ namespace Tgstation.Server.Host.Components.Deployment // This happens when we're told to load the compile job that is currently finished up // It constitutes an API violation if it's returned by the DreamDaemonController so just set it here // Bit of a hack, but it works out to be nearly if not the same value that's put in the DB - logger.LogTrace("Setting missing StoppedAt for CompileJob.Job #{0}...", compileJob.Job.Id); + logger.LogTrace("Setting missing StoppedAt for CompileJob.Job #{id}...", compileJob.Job.Id); compileJob.Job.StoppedAt = DateTimeOffset.UtcNow; } @@ -288,7 +297,7 @@ namespace Tgstation.Server.Host.Components.Deployment // rebuild the provider because it's using the legacy style directories // Don't dispose it - logger.LogDebug("Creating legacy two folder .dmb provider targeting {0} directory...", LegacyADirectoryName); + logger.LogDebug("Creating legacy two folder .dmb provider targeting {aDirName} directory...", LegacyADirectoryName); newProvider = new DmbProvider(compileJob, ioManager, CleanupAction, Path.DirectorySeparatorChar + LegacyADirectoryName); } @@ -304,7 +313,7 @@ namespace Tgstation.Server.Host.Components.Deployment providerSubmitted = true; - logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value); + logger.LogTrace("Compile job {id} lock count now: {lockCount}", compileJob.Id, value); return newProvider; } } @@ -324,7 +333,7 @@ namespace Tgstation.Server.Host.Components.Deployment // don't clean locked directories lock (jobLockCounts) - jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList(); + jobIdsToSkip = jobLockCounts.Keys.ToList(); List jobUidsToNotErase = null; @@ -350,7 +359,7 @@ namespace Tgstation.Server.Host.Components.Deployment jobUidsToNotErase.Add(SwappableDmbProvider.LiveGameDirectory); - logger.LogTrace("We will not clean the following directories: {0}", String.Join(", ", jobUidsToNotErase)); + logger.LogTrace("We will not clean the following directories: {directoriesToNotClean}", String.Join(", ", jobUidsToNotErase)); // cleanup var gameDirectory = ioManager.ResolvePath(); @@ -362,7 +371,7 @@ namespace Tgstation.Server.Host.Components.Deployment var nameOnly = ioManager.GetFileName(x); if (jobUidsToNotErase.Contains(nameOnly)) return; - logger.LogDebug("Cleaning unused game folder: {0}...", nameOnly); + logger.LogDebug("Cleaning unused game folder: {dirName}...", nameOnly); try { ++deleting; @@ -374,7 +383,7 @@ namespace Tgstation.Server.Host.Components.Deployment } catch (Exception e) { - logger.LogWarning(e, "Error deleting directory {0}!", x); + logger.LogWarning(e, "Error deleting directory {dirName}!", x); } }).ToList(); if (deleting > 0) @@ -427,13 +436,13 @@ namespace Tgstation.Server.Host.Components.Deployment if (currentVal == 1) { jobLockCounts.Remove(job.Id.Value); - logger.LogDebug("Cleaning lock-free compile job {0} => {1}", job.Id, job.DirectoryName); + logger.LogDebug("Cleaning lock-free compile job {id} => {dirName}", job.Id, job.DirectoryName); cleanupTask = HandleCleanup(); } else { var decremented = --jobLockCounts[job.Id.Value]; - logger.LogTrace("Compile job {0} lock count now: {1}", job.Id, decremented); + logger.LogTrace("Compile job {id} lock count now: {lockCount}", job.Id, decremented); } else logger.LogError("Extra Dispose of DmbProvider for CompileJob {compileJobId}!", job.Id); diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index ff584a4ab1..665efba795 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -117,7 +117,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// The active callback from . /// - Action currentChatCallback; + Func> currentChatCallback; /// /// Cached for . @@ -355,7 +355,8 @@ namespace Tgstation.Server.Host.Components.Deployment logger.LogTrace("Created CompileJob {compileJobId}", compileJob.Id); try { - await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken); + var chatNotificationAction = currentChatCallback(null, compileJob.Output); + await compileJobConsumer.LoadCompileJob(compileJob, chatNotificationAction, cancellationToken); } catch { @@ -389,8 +390,6 @@ namespace Tgstation.Server.Host.Components.Deployment try { - currentChatCallback(null, compileJob.Output); - await Task.WhenAll(commentsTask, eventTask); } catch (Exception ex) diff --git a/src/Tgstation.Server.Host/Components/Deployment/ICompileJobSink.cs b/src/Tgstation.Server.Host/Components/Deployment/ICompileJobSink.cs index 0eda36354f..086301c87f 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/ICompileJobSink.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/ICompileJobSink.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Models; @@ -14,8 +15,9 @@ namespace Tgstation.Server.Host.Components.Deployment /// Load a new into the . /// /// The to load. + /// An to be called when the becomes active or is discarded with or respectively. /// The for the operation. /// A representing the running operation. - Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken); + Task LoadCompileJob(CompileJob job, Action activationAction, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs index c75c3832bf..6a4790a62e 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -26,15 +27,25 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote /// protected ILogger Logger { get; } + /// + /// A map of s to activation callback s. + /// + readonly ConcurrentDictionary> activationCallbacks; + /// /// Initializes a new instance of the class. /// /// The value of . /// The value of . - protected BaseRemoteDeploymentManager(ILogger logger, Api.Models.Instance metadata) + /// The value of . + protected BaseRemoteDeploymentManager( + ILogger logger, + Api.Models.Instance metadata, + ConcurrentDictionary> activationCallbacks) { Logger = logger ?? throw new ArgumentNullException(nameof(logger)); Metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); + this.activationCallbacks = activationCallbacks ?? throw new ArgumentNullException(nameof(activationCallbacks)); } /// @@ -58,7 +69,6 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote previousRevisionInformation.ActiveTestMerges ??= new List(); deployedRevisionInformation.ActiveTestMerges ??= new List(); - var tasks = new List(); // added prs var addedTestMerges = deployedRevisionInformation @@ -87,10 +97,12 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote return; Logger.LogTrace( - "Commenting on {0} added, {1} removed, and {2} updated test merge sources...", + "Commenting on {addedCount} added, {removedCount} removed, and {updatedCount} updated test merge sources...", addedTestMerges.Count, removedTestMerges.Count, updatedTestMerges.Count); + + var tasks = new List(addedTestMerges.Count + updatedTestMerges.Count + removedTestMerges.Count); foreach (var addedTestMerge in addedTestMerges) tasks.Add( CommentOnTestMergeSource( @@ -138,13 +150,29 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote } /// - public abstract Task ApplyDeployment(CompileJob compileJob, CompileJob oldCompileJob, CancellationToken cancellationToken); + public Task ApplyDeployment(CompileJob compileJob, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(compileJob); + + if (activationCallbacks.TryGetValue(compileJob.Id.Value, out var activationCallback)) + activationCallback(true); + + return ApplyDeploymentImpl(compileJob, cancellationToken); + } /// public abstract Task FailDeployment(CompileJob compileJob, string errorMessage, CancellationToken cancellationToken); /// - public abstract Task MarkInactive(CompileJob compileJob, CancellationToken cancellationToken); + public Task MarkInactive(CompileJob compileJob, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(compileJob); + + if (activationCallbacks.TryRemove(compileJob.Id.Value, out var activationCallback)) + activationCallback(false); + + return MarkInactiveImpl(compileJob, cancellationToken); + } /// public abstract Task> RemoveMergedTestMerges( @@ -154,7 +182,15 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote CancellationToken cancellationToken); /// - public abstract Task StageDeployment(CompileJob compileJob, CancellationToken cancellationToken); + public Task StageDeployment(CompileJob compileJob, Action activationCallback, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(compileJob); + + if (activationCallback != null && !activationCallbacks.TryAdd(compileJob.Id.Value, activationCallback)) + Logger.LogError("activationCallbacks conflicted on CompileJob #{id}!", compileJob.Id.Value); + + return StageDeploymentImpl(compileJob, cancellationToken); + } /// public abstract Task StartDeployment( @@ -162,6 +198,30 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote CompileJob compileJob, CancellationToken cancellationToken); + /// + /// Implementation of . + /// + /// The staged . + /// The for the operation. + /// A representing the running operation. + protected abstract Task StageDeploymentImpl(CompileJob compileJob, CancellationToken cancellationToken); + + /// + /// Implementation of . + /// + /// The being applied. + /// The for the operation. + /// A representing the running operation. + protected abstract Task ApplyDeploymentImpl(CompileJob compileJob, CancellationToken cancellationToken); + + /// + /// Implementation of . + /// + /// The inactive . + /// The for the operation. + /// A representing the running operation. + protected abstract Task MarkInactiveImpl(CompileJob compileJob, CancellationToken cancellationToken); + /// /// Formats a comment for a given . /// diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs index 7365a58089..aa9fc5c0a4 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; @@ -40,12 +41,14 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote /// The value of . /// The for the . /// The for the . + /// The activation callback for the . public GitHubRemoteDeploymentManager( IDatabaseContextFactory databaseContextFactory, IGitHubServiceFactory gitHubServiceFactory, ILogger logger, - Api.Models.Instance metadata) - : base(logger, metadata) + Api.Models.Instance metadata, + ConcurrentDictionary> activationCallbacks) + : base(logger, metadata, activationCallbacks) { this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory)); @@ -144,24 +147,6 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote } } - /// - public override Task StageDeployment( - CompileJob compileJob, - CancellationToken cancellationToken) - => UpdateDeployment( - compileJob, - "The deployment succeeded and will be applied a the next server reboot.", - DeploymentState.Pending, - cancellationToken); - - /// - public override Task ApplyDeployment(CompileJob compileJob, CompileJob oldCompileJob, CancellationToken cancellationToken) - => UpdateDeployment( - compileJob, - "The deployment is now live on the server.", - DeploymentState.Success, - cancellationToken); - /// public override Task FailDeployment(CompileJob compileJob, string errorMessage, CancellationToken cancellationToken) => UpdateDeployment( @@ -170,14 +155,6 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote DeploymentState.Error, cancellationToken); - /// - public override Task MarkInactive(CompileJob compileJob, CancellationToken cancellationToken) - => UpdateDeployment( - compileJob, - "The deployment has been superceeded.", - DeploymentState.Inactive, - cancellationToken); - /// public override async Task> RemoveMergedTestMerges( IRepository repository, @@ -237,6 +214,32 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote return newList; } + /// + protected override Task StageDeploymentImpl( + CompileJob compileJob, + CancellationToken cancellationToken) + => UpdateDeployment( + compileJob, + "The deployment succeeded and will be applied a the next server reboot.", + DeploymentState.Pending, + cancellationToken); + + /// + protected override Task ApplyDeploymentImpl(CompileJob compileJob, CancellationToken cancellationToken) + => UpdateDeployment( + compileJob, + "The deployment is now live on the server.", + DeploymentState.Success, + cancellationToken); + + /// + protected override Task MarkInactiveImpl(CompileJob compileJob, CancellationToken cancellationToken) + => UpdateDeployment( + compileJob, + "The deployment has been superceeded.", + DeploymentState.Inactive, + cancellationToken); + /// protected override async Task CommentOnTestMergeSource( RepositorySettings repositorySettings, diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs index d9b8c99a08..18e172c122 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -25,8 +26,12 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote /// /// The for the . /// The for the . - public GitLabRemoteDeploymentManager(ILogger logger, Api.Models.Instance metadata) - : base(logger, metadata) + /// The activation callback for the . + public GitLabRemoteDeploymentManager( + ILogger logger, + Api.Models.Instance metadata, + ConcurrentDictionary> activationCallbacks) + : base(logger, metadata, activationCallbacks) { } @@ -94,30 +99,29 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote return newList; } - /// - public override Task ApplyDeployment( - CompileJob compileJob, - CompileJob oldCompileJob, - CancellationToken cancellationToken) => Task.CompletedTask; - /// public override Task FailDeployment( CompileJob compileJob, string errorMessage, CancellationToken cancellationToken) => Task.CompletedTask; - /// - public override Task MarkInactive(CompileJob compileJob, CancellationToken cancellationToken) => Task.CompletedTask; - - /// - public override Task StageDeployment(CompileJob compileJob, CancellationToken cancellationToken) => Task.CompletedTask; - /// public override Task StartDeployment( Api.Models.Internal.IGitRemoteInformation remoteInformation, CompileJob compileJob, CancellationToken cancellationToken) => Task.CompletedTask; + /// + protected override Task ApplyDeploymentImpl( + CompileJob compileJob, + CancellationToken cancellationToken) => Task.CompletedTask; + + /// + protected override Task StageDeploymentImpl(CompileJob compileJob, CancellationToken cancellationToken) => Task.CompletedTask; + + /// + protected override Task MarkInactiveImpl(CompileJob compileJob, CancellationToken cancellationToken) => Task.CompletedTask; + /// protected override async Task CommentOnTestMergeSource( RepositorySettings repositorySettings, diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs index 4c3f357cda..ffa64bc878 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -28,20 +29,21 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote /// Stage a given 's deployment. /// /// The staged . + /// An optional to be called when the becomes active or is discarded with or respectively. /// The for the operation. /// A representing the running operation. Task StageDeployment( CompileJob compileJob, + Action activationCallback, CancellationToken cancellationToken); /// /// Stage a given 's deployment. /// /// The being applied. - /// The currently active , if any. /// The for the operation. /// A representing the running operation. - Task ApplyDeployment(CompileJob compileJob, CompileJob oldCompileJob, CancellationToken cancellationToken); + Task ApplyDeployment(CompileJob compileJob, CancellationToken cancellationToken); /// /// Fail a deployment for a given . diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManagerFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManagerFactory.cs index 690b39d2ce..b3dce1f69c 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManagerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManagerFactory.cs @@ -1,4 +1,6 @@ -using Tgstation.Server.Api.Models; +using System.Collections.Generic; + +using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Components.Deployment.Remote { @@ -22,5 +24,11 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote /// The containing the to use. /// A new . IRemoteDeploymentManager CreateRemoteDeploymentManager(Api.Models.Instance metadata, Models.CompileJob compileJob); + + /// + /// Cause the to drop any local state is has for the given . + /// + /// An of s. + void ForgetLocalStateForCompileJobs(IEnumerable compileJobsIds); } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/NoOpRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/NoOpRemoteDeploymentManager.cs index 069b8912fa..0aee4358fd 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/NoOpRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/NoOpRemoteDeploymentManager.cs @@ -1,8 +1,12 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Models; @@ -11,48 +15,66 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote /// /// No-op implementation of . /// - sealed class NoOpRemoteDeploymentManager : IRemoteDeploymentManager + sealed class NoOpRemoteDeploymentManager : BaseRemoteDeploymentManager { - /// - public Task ApplyDeployment( - CompileJob compileJob, - CompileJob oldCompileJob, - CancellationToken cancellationToken) => Task.CompletedTask; + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The activation callback for the . + public NoOpRemoteDeploymentManager( + ILogger logger, + Api.Models.Instance metadata, + ConcurrentDictionary> activationCallbacks) + : base(logger, metadata, activationCallbacks) + { + } /// - public Task FailDeployment( - CompileJob compileJob, - string errorMessage, - CancellationToken cancellationToken) => Task.CompletedTask; + public override Task FailDeployment(Models.CompileJob compileJob, string errorMessage, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } /// - public Task MarkInactive( - CompileJob compileJob, - CancellationToken cancellationToken) => Task.CompletedTask; + public override Task> RemoveMergedTestMerges(IRepository repository, Models.RepositorySettings repositorySettings, Models.RevisionInformation revisionInformation, CancellationToken cancellationToken) + => Task.FromResult>(Array.Empty()); /// - public Task PostDeploymentComments( - CompileJob compileJob, - RevisionInformation previousRevisionInformation, - RepositorySettings repositorySettings, - string repoOwner, - string repoName, - CancellationToken cancellationToken) => Task.CompletedTask; + public override Task StartDeployment(IGitRemoteInformation remoteInformation, Models.CompileJob compileJob, CancellationToken cancellationToken) + => Task.CompletedTask; /// - public Task> RemoveMergedTestMerges( - IRepository repository, - RepositorySettings repositorySettings, - RevisionInformation revisionInformation, - CancellationToken cancellationToken) => Task.FromResult>(Array.Empty()); + protected override Task ApplyDeploymentImpl(Models.CompileJob compileJob, CancellationToken cancellationToken) => Task.CompletedTask; /// - public Task StageDeployment(CompileJob compileJob, CancellationToken cancellationToken) => Task.CompletedTask; + protected override Task CommentOnTestMergeSource( + Models.RepositorySettings repositorySettings, + string remoteRepositoryOwner, + string remoteRepositoryName, + string comment, + int testMergeNumber, + CancellationToken cancellationToken) + => Task.CompletedTask; /// - public Task StartDeployment( - Api.Models.Internal.IGitRemoteInformation remoteInformation, - CompileJob compileJob, - CancellationToken cancellationToken) => Task.CompletedTask; + protected override string FormatTestMerge( + Models.RepositorySettings repositorySettings, + Models.CompileJob compileJob, + TestMerge testMerge, + string remoteRepositoryOwner, + string remoteRepositoryName, + bool updated) + => String.Empty; + + /// + protected override Task MarkInactiveImpl(Models.CompileJob compileJob, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + /// + protected override Task StageDeploymentImpl(Models.CompileJob compileJob, CancellationToken cancellationToken) => Task.CompletedTask; } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs index e05f885241..0fdce864cf 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using Microsoft.Extensions.Logging; @@ -37,6 +39,11 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote /// readonly ILogger logger; + /// + /// A map of s to activation callback s. + /// + readonly ConcurrentDictionary> activationCallbacks; + /// /// Initializes a new instance of the class. /// @@ -57,6 +64,8 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + activationCallbacks = new ConcurrentDictionary>(); } /// @@ -71,11 +80,16 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote databaseContextFactory, gitHubServiceFactory, loggerFactory.CreateLogger(), - metadata), + metadata, + activationCallbacks), RemoteGitProvider.GitLab => new GitLabRemoteDeploymentManager( loggerFactory.CreateLogger(), - metadata), - RemoteGitProvider.Unknown => new NoOpRemoteDeploymentManager(), + metadata, + activationCallbacks), + RemoteGitProvider.Unknown => new NoOpRemoteDeploymentManager( + loggerFactory.CreateLogger(), + metadata, + activationCallbacks), _ => throw new InvalidOperationException($"Invalid RemoteGitProvider: {remoteGitProvider}!"), }; } @@ -97,5 +111,14 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote return CreateRemoteDeploymentManager(metadata, remoteGitProvider); } + + /// + public void ForgetLocalStateForCompileJobs(IEnumerable compileJobIds) + { + ArgumentNullException.ThrowIfNull(compileJobIds); + + foreach (var compileJobId in compileJobIds) + activationCallbacks.Remove(compileJobId, out _); + } } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 09474acdb5..da8675e21e 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -680,7 +680,7 @@ namespace Tgstation.Server.Host.Components.Watchdog try { - await remoteDeploymentManager.ApplyDeployment(newCompileJob, ActiveCompileJob, cancellationToken); + await remoteDeploymentManager.ApplyDeployment(newCompileJob, cancellationToken); } catch (Exception ex) { diff --git a/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs b/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs index 51dd73083d..214b9679a3 100644 --- a/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs +++ b/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs @@ -100,7 +100,7 @@ namespace Tgstation.Server.Tests.Live return Task.CompletedTask; } - public override Task> SendUpdateMessage(RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, string gitHubOwner, string gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken) + public override Task>>> SendUpdateMessage(RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, string gitHubOwner, string gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(revisionInformation); ArgumentNullException.ThrowIfNull(byondVersion); @@ -115,7 +115,7 @@ namespace Tgstation.Server.Tests.Live if (random.Next(0, 100) > 70) throw new Exception("Random SendUpdateMessage failure!"); */ - return Task.FromResult>((_, _) => + return Task.FromResult>>>((_, _) => { cancellationToken.ThrowIfCancellationRequested(); @@ -123,7 +123,7 @@ namespace Tgstation.Server.Tests.Live if (random.Next(0, 100) > 70) throw new Exception("Random SendUpdateMessage failure!"); */ - return Task.CompletedTask; + return Task.FromResult>(_ => Task.CompletedTask); }); }