diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index d8d4230ee6..9d19b127e7 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -534,7 +534,7 @@ namespace Tgstation.Server.Host.Components.Deployment #pragma warning disable CA1506 public async Task DeploymentProcess( Models.Job job, - IDatabaseContext databaseContext, + IDatabaseContextFactory databaseContextFactory, Action progressReporter, CancellationToken cancellationToken) { @@ -542,44 +542,41 @@ namespace Tgstation.Server.Host.Components.Deployment if (job == null) throw new ArgumentNullException(nameof(job)); #pragma warning restore IDE0016 // Use 'throw' expression - if (databaseContext == null) - throw new ArgumentNullException(nameof(databaseContext)); + if (databaseContextFactory == null) + throw new ArgumentNullException(nameof(databaseContextFactory)); if (progressReporter == null) throw new ArgumentNullException(nameof(progressReporter)); - var averageSpan = await CalculateExpectedDeploymentTime(databaseContext, cancellationToken).ConfigureAwait(false); - - var ddSettings = await databaseContext - .DreamDaemonSettings - .Where(x => x.InstanceId == metadata.Id) - .Select(x => new Models.DreamDaemonSettings - { - StartupTimeout = x.StartupTimeout, - }) - .FirstOrDefaultAsync(cancellationToken) - .ConfigureAwait(false); - if (ddSettings == default) - throw new JobException(ErrorCode.InstanceMissingDreamDaemonSettings); - - var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false); - if (dreamMakerSettings == default) - throw new JobException(ErrorCode.InstanceMissingDreamMakerSettings); - - Models.RepositorySettings repositorySettings = null; string repoOwner = null; string repoName = null; - Models.CompileJob compileJob; - Models.RevisionInformation revInfo; - - using (var repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) - { - if (repo == null) - throw new JobException(ErrorCode.RepoMissing); - - if (repo.IsGitHubRepository) + TimeSpan? averageSpan = null; + Models.RepositorySettings repositorySettings = null; + Models.DreamDaemonSettings ddSettings = null; + DreamMakerSettings dreamMakerSettings = null; + IRepository repo = null; + Models.CompileJob compileJob = null; + Models.RevisionInformation revInfo = null; + await databaseContextFactory.UseContext( + async databaseContext => { - repoOwner = repo.GitHubOwner; - repoName = repo.GitHubRepoName; + averageSpan = await CalculateExpectedDeploymentTime(databaseContext, cancellationToken).ConfigureAwait(false); + + ddSettings = await databaseContext + .DreamDaemonSettings + .Where(x => x.InstanceId == metadata.Id) + .Select(x => new Models.DreamDaemonSettings + { + StartupTimeout = x.StartupTimeout, + }) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + if (ddSettings == default) + throw new JobException(ErrorCode.InstanceMissingDreamDaemonSettings); + + dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false); + if (dreamMakerSettings == default) + throw new JobException(ErrorCode.InstanceMissingDreamMakerSettings); + repositorySettings = await databaseContext .RepositorySettings .Where(x => x.InstanceId == metadata.Id) @@ -594,34 +591,55 @@ namespace Tgstation.Server.Host.Components.Deployment .ConfigureAwait(false); if (repositorySettings == default) throw new JobException(ErrorCode.InstanceMissingRepositorySettings); - } - var repoSha = repo.Head; - revInfo = await databaseContext - .RevisionInformations - .Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id) - .Include(x => x.ActiveTestMerges) - .ThenInclude(x => x.TestMerge) - .ThenInclude(x => x.MergedBy) - .FirstOrDefaultAsync(cancellationToken) - .ConfigureAwait(false); - - if (revInfo == default) - { - revInfo = new Models.RevisionInformation + repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false); + try { - CommitSha = repoSha, - OriginCommitSha = repoSha, - Instance = new Models.Instance + if (repo == null) + throw new JobException(ErrorCode.RepoMissing); + + if (repo.IsGitHubRepository) { - Id = metadata.Id + repoOwner = repo.GitHubOwner; + repoName = repo.GitHubRepoName; } - }; - logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha); - databaseContext.Instances.Attach(revInfo.Instance); - } + var repoSha = repo.Head; + revInfo = await databaseContext + .RevisionInformations + .Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id) + .Include(x => x.ActiveTestMerges) + .ThenInclude(x => x.TestMerge) + .ThenInclude(x => x.MergedBy) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + if (revInfo == default) + { + revInfo = new Models.RevisionInformation + { + CommitSha = repoSha, + OriginCommitSha = repoSha, + Instance = new Models.Instance + { + Id = metadata.Id + } + }; + + logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha); + databaseContext.Instances.Attach(revInfo.Instance); + await databaseContext.Save(cancellationToken).ConfigureAwait(false); + } + } + catch + { + repo.Dispose(); + throw; + } + }) + .ConfigureAwait(false); + + using (repo) compileJob = await Compile( revInfo, dreamMakerSettings, @@ -632,33 +650,46 @@ namespace Tgstation.Server.Host.Components.Deployment cancellationToken) .ConfigureAwait(false); - try - { - compileJob.Job = job; - - databaseContext.CompileJobs.Add(compileJob); - - await PostDeploymentComments(compileJob, repositorySettings, repoOwner, repoName).ConfigureAwait(false); - - // The difficulty with compile jobs is they have a two part commit - await databaseContext.Save(cancellationToken).ConfigureAwait(false); - try + try + { + await databaseContextFactory.UseContext( + async databaseContext => { - await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false); - } - catch - { - // So we need to un-commit the compile job if the above throws - databaseContext.CompileJobs.Remove(compileJob); - await databaseContext.Save(default).ConfigureAwait(false); - throw; - } - } - catch (Exception ex) - { - await CleanupFailedCompile(compileJob, ex is OperationCanceledException, default).ConfigureAwait(false); - throw; - } + compileJob.Job = new Models.Job + { + Id = job.Id + }; + compileJob.RevisionInformation = new Models.RevisionInformation + { + Id = revInfo.Id + }; + + databaseContext.RevisionInformations.Attach(compileJob.RevisionInformation); + databaseContext.Jobs.Attach(compileJob.Job); + databaseContext.CompileJobs.Add(compileJob); + + await PostDeploymentComments(compileJob, repositorySettings, repoOwner, repoName).ConfigureAwait(false); + + // The difficulty with compile jobs is they have a two part commit + await databaseContext.Save(cancellationToken).ConfigureAwait(false); + try + { + await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false); + } + catch + { + // So we need to un-commit the compile job if the above throws + databaseContext.CompileJobs.Remove(compileJob); + await databaseContext.Save(default).ConfigureAwait(false); + throw; + } + }) + .ConfigureAwait(false); + } + catch (Exception ex) + { + await CleanupFailedCompile(compileJob, ex is OperationCanceledException, default).ConfigureAwait(false); + throw; } await eventConsumer.HandleEvent(EventType.DeploymentComplete, null, cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Deployment/IDreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/IDreamMaker.cs index e23a5c224f..a180138050 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/IDreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/IDreamMaker.cs @@ -15,13 +15,13 @@ namespace Tgstation.Server.Host.Components.Deployment /// Create and a compile job and insert it into the database. Meant to be called by a . /// /// The running . - /// The for the operation. + /// The for the operation. /// The to report compilation progress. /// The for the operation. /// A representing the running operation. Task DeploymentProcess( Job job, - IDatabaseContext databaseContext, + IDatabaseContextFactory databaseContextFactory, Action progressReporter, CancellationToken cancellationToken); } diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 38bc7fbbc2..d1c281352b 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -179,148 +179,156 @@ namespace Tgstation.Server.Host.Components }; string deploySha = null; - await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContext, progressReporter, jobCancellationToken) => + await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContextFactory, progressReporter, jobCancellationToken) => { - var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken); - // assume 5 steps with synchronize const int ProgressSections = 7; const int ProgressStep = 100 / ProgressSections; + string repoHead = null; - const int NumSteps = 3; - var doneSteps = 0; - - Action NextProgressReporter() - { - var tmpDoneSteps = doneSteps; - ++doneSteps; - return progress => progressReporter((progress + (100 * tmpDoneSteps)) / NumSteps); - } - - using var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false); - if (repo == null) - { - logger.LogTrace("Aborting repo update, no repository!"); - return; - } - - var startSha = repo.Head; - if (!repo.Tracking) - { - logger.LogTrace("Aborting repo update, active ref not tracking any remote branch!"); - deploySha = startSha; - return; - } - - var repositorySettings = await repositorySettingsTask.ConfigureAwait(false); - - // the main point of auto update is to pull the remote - await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); - - RevisionInformation currentRevInfo = null; - bool hasDbChanges = false; - - Task LoadRevInfo() => databaseContext.RevisionInformations - .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id) - .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge) - .FirstOrDefaultAsync(cancellationToken); - - async Task UpdateRevInfo(string currentHead, bool onOrigin) - { - if (currentRevInfo == null) - currentRevInfo = await LoadRevInfo().ConfigureAwait(false); - - if (currentRevInfo == default) + await databaseContextFactory.UseContext( + async databaseContext => { - logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead); - onOrigin = true; - } + var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken); - var attachedInstance = new Models.Instance - { - Id = metadata.Id - }; - var oldRevInfo = currentRevInfo; - currentRevInfo = new RevisionInformation - { - CommitSha = currentHead, - OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha, - Instance = attachedInstance - }; - if (!onOrigin) - currentRevInfo.ActiveTestMerges = new List(oldRevInfo.ActiveTestMerges); + const int NumSteps = 3; + var doneSteps = 0; - databaseContext.Instances.Attach(attachedInstance); - databaseContext.RevisionInformations.Add(currentRevInfo); - hasDbChanges = true; - } + Action NextProgressReporter() + { + var tmpDoneSteps = doneSteps; + ++doneSteps; + return progress => progressReporter((progress + (100 * tmpDoneSteps)) / NumSteps); + } - // take appropriate auto update actions - bool shouldSyncTracked; - if (repositorySettings.AutoUpdatesKeepTestMerges.Value) - { - logger.LogTrace("Preserving test merges..."); + using var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false); + if (repo == null) + { + logger.LogTrace("Aborting repo update, no repository!"); + return; + } - var currentRevInfoTask = LoadRevInfo(); + var startSha = repo.Head; + if (!repo.Tracking) + { + logger.LogTrace("Aborting repo update, active ref not tracking any remote branch!"); + deploySha = startSha; + return; + } - var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); + var repositorySettings = await repositorySettingsTask.ConfigureAwait(false); - if (!result.HasValue) - throw new JobException(Api.Models.ErrorCode.InstanceUpdateTestMergeConflict); + // the main point of auto update is to pull the remote + await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); - currentRevInfo = await currentRevInfoTask.ConfigureAwait(false); + RevisionInformation currentRevInfo = null; + bool hasDbChanges = false; - var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha; - var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit; + Task LoadRevInfo() => databaseContext.RevisionInformations + .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id) + .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge) + .FirstOrDefaultAsync(cancellationToken); - var currentHead = repo.Head; - if (currentHead != startSha) - { - await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false); - shouldSyncTracked = stillOnOrigin; - } - else - shouldSyncTracked = false; - } - else - { - logger.LogTrace("Not preserving test merges..."); - await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); + async Task UpdateRevInfo(string currentHead, bool onOrigin) + { + if (currentRevInfo == null) + currentRevInfo = await LoadRevInfo().ConfigureAwait(false); - var currentHead = repo.Head; + if (currentRevInfo == default) + { + logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead); + onOrigin = true; + } - currentRevInfo = await databaseContext.RevisionInformations - .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id) - .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false); + var attachedInstance = new Models.Instance + { + Id = metadata.Id + }; + var oldRevInfo = currentRevInfo; + currentRevInfo = new RevisionInformation + { + CommitSha = currentHead, + OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha, + Instance = attachedInstance + }; + if (!onOrigin) + currentRevInfo.ActiveTestMerges = new List(oldRevInfo.ActiveTestMerges); - if (currentHead != startSha && currentRevInfo != default) - await UpdateRevInfo(currentHead, true).ConfigureAwait(false); + databaseContext.Instances.Attach(attachedInstance); + databaseContext.RevisionInformations.Add(currentRevInfo); + hasDbChanges = true; + } - shouldSyncTracked = true; - } + // take appropriate auto update actions + bool shouldSyncTracked; + if (repositorySettings.AutoUpdatesKeepTestMerges.Value) + { + logger.LogTrace("Preserving test merges..."); - // synch if necessary - if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head) - { - var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false); - var currentHead = repo.Head; - if (currentHead != currentRevInfo.CommitSha) - await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false); - } + var currentRevInfoTask = LoadRevInfo(); - if (hasDbChanges) - try - { - await databaseContext.Save(cancellationToken).ConfigureAwait(false); - } - catch - { - await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false); - throw; - } + var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); + + if (!result.HasValue) + throw new JobException(Api.Models.ErrorCode.InstanceUpdateTestMergeConflict); + + currentRevInfo = await currentRevInfoTask.ConfigureAwait(false); + + var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha; + var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit; + + var currentHead = repo.Head; + if (currentHead != startSha) + { + await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false); + shouldSyncTracked = stillOnOrigin; + } + else + shouldSyncTracked = false; + } + else + { + logger.LogTrace("Not preserving test merges..."); + await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); + + var currentHead = repo.Head; + + currentRevInfo = await databaseContext.RevisionInformations + .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id) + .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false); + + if (currentHead != startSha && currentRevInfo != default) + await UpdateRevInfo(currentHead, true).ConfigureAwait(false); + + shouldSyncTracked = true; + } + + // synch if necessary + if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head) + { + var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false); + var currentHead = repo.Head; + if (currentHead != currentRevInfo.CommitSha) + await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false); + } + + repoHead = repo.Head; + + if (hasDbChanges) + try + { + await databaseContext.Save(cancellationToken).ConfigureAwait(false); + } + catch + { + await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false); + throw; + } + }) + .ConfigureAwait(false); progressReporter(5 * ProgressStep); - deploySha = repo.Head; + deploySha = repoHead; }, cancellationToken).ConfigureAwait(false); await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 7a00c1e20d..9be0802cc7 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -211,8 +211,7 @@ namespace Tgstation.Server.Host.Components repoIoManager, eventConsumer, loggerFactory.CreateLogger(), - loggerFactory.CreateLogger(), - metadata.RepositorySettings); + loggerFactory.CreateLogger()); try { var byond = new ByondManager(byondIOManager, byondInstaller, eventConsumer, loggerFactory.CreateLogger()); diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index fc053ed6cf..a8b50e97d3 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -3,9 +3,10 @@ using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Tasks; -using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Jobs; namespace Tgstation.Server.Host.Components.Repository { @@ -48,11 +49,6 @@ namespace Tgstation.Server.Host.Components.Repository /// readonly ILogger logger; - /// - /// The for the - /// - readonly RepositorySettings repositorySettings; - /// /// Used for controlling single access to the /// @@ -67,15 +63,13 @@ namespace Tgstation.Server.Host.Components.Repository /// The value of /// The value of /// The value of - /// The value of public RepositoryManager( ILibGit2RepositoryFactory repositoryFactory, ILibGit2Commands commands, IIOManager ioManager, IEventConsumer eventConsumer, ILogger repositoryLogger, - ILogger logger, - RepositorySettings repositorySettings) + ILogger logger) { this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory)); this.commands = commands ?? throw new ArgumentNullException(nameof(commands)); @@ -83,7 +77,6 @@ namespace Tgstation.Server.Host.Components.Repository this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); this.repositoryLogger = repositoryLogger ?? throw new ArgumentNullException(nameof(repositoryLogger)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.repositorySettings = repositorySettings ?? throw new ArgumentNullException(nameof(repositorySettings)); semaphore = new SemaphoreSlim(1); } @@ -106,7 +99,7 @@ namespace Tgstation.Server.Host.Components.Repository lock (semaphore) { if (CloneInProgress) - throw new InvalidOperationException("The repository is already being cloned!"); + throw new JobException(ErrorCode.RepoCloning); CloneInProgress = true; } @@ -179,33 +172,33 @@ namespace Tgstation.Server.Host.Components.Repository logger.LogTrace("Begin LoadRepository..."); lock (semaphore) if (CloneInProgress) - throw new InvalidOperationException("The repository is being cloned!"); - using (var context = await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) - try - { - var libGitRepo = await repositoryFactory.CreateFromPath(ioManager.ResolvePath(), cancellationToken).ConfigureAwait(false); + throw new JobException(ErrorCode.RepoCloning); + using var context = await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false); + try + { + var libGitRepo = await repositoryFactory.CreateFromPath(ioManager.ResolvePath(), cancellationToken).ConfigureAwait(false); - if (libGitRepo == null) - return null; - - return new Repository( - libGitRepo, - commands, - ioManager, - eventConsumer, - repositoryFactory, - repositoryLogger, () => - { - logger.LogTrace("Releasing semaphore due to Repository disposal..."); - semaphore.Release(); - }); - } - catch (RepositoryNotFoundException e) - { - logger.LogDebug("Repository not found!"); - logger.LogTrace("Exception: {0}", e); + if (libGitRepo == null) return null; - } + + return new Repository( + libGitRepo, + commands, + ioManager, + eventConsumer, + repositoryFactory, + repositoryLogger, () => + { + logger.LogTrace("Releasing semaphore due to Repository disposal..."); + semaphore.Release(); + }); + } + catch (RepositoryNotFoundException e) + { + logger.LogDebug("Repository not found!"); + logger.LogTrace("Exception: {0}", e); + return null; + } } /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index b694624a7d..7312a3f6d7 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -865,7 +865,7 @@ namespace Tgstation.Server.Host.Components.Watchdog CancelRight = (ulong)DreamDaemonRights.Shutdown, CancelRightsType = RightsType.DreamDaemon }; - await jobManager.RegisterOperation(job, async (j, databaseContext, progressFunction, ct) => + await jobManager.RegisterOperation(job, async (j, databaseContextFactory, progressFunction, ct) => { using (await SemaphoreSlimContext.Lock(Semaphore, ct).ConfigureAwait(false)) await LaunchImplNoLock(true, true, reattachInfo, ct).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index a2c6f811d7..ed4a042794 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -120,7 +120,7 @@ namespace Tgstation.Server.Host.Controllers CancelRight = (ulong)ByondRights.CancelInstall, Instance = Instance }; - await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false); result.InstallJob = job.ToApi(); } diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index 80890b721c..bae044577b 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -77,7 +77,7 @@ namespace Tgstation.Server.Host.Controllers Instance = Instance, StartedBy = AuthenticationContext.User }; - await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, innerCt) => instance.Watchdog.Launch(innerCt), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, innerCt) => instance.Watchdog.Launch(innerCt), cancellationToken).ConfigureAwait(false); return Accepted(job.ToApi()); } @@ -274,7 +274,7 @@ namespace Tgstation.Server.Host.Controllers var watchdog = instanceManager.GetInstance(Instance).Watchdog; - await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false); return Accepted(job.ToApi()); } } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index c4e1dc86cf..ed76728410 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -505,7 +505,7 @@ namespace Tgstation.Server.Host.Controllers StartedBy = AuthenticationContext.User }; - await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false); api.MoveJob = job.ToApi(); } diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 9e70de265a..03c5eed46e 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -82,7 +82,11 @@ namespace Tgstation.Server.Host.Controllers // If the DB doesn't have it, check the local set if (revisionInfo == default) - revisionInfo = databaseContext.RevisionInformations.Local.Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id).FirstOrDefault(); + revisionInfo = databaseContext + .RevisionInformations + .Local + .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id) + .FirstOrDefault(); var needsDbUpdate = revisionInfo == default; if (needsDbUpdate) @@ -193,7 +197,7 @@ namespace Tgstation.Server.Host.Controllers Instance = Instance }; var api = currentModel.ToApi(); - await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) => + await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) => { using var repos = await repoManager.CloneRepository(new Uri(origin), cloneBranch, currentModel.AccessUser, currentModel.AccessToken, progressReporter, ct).ConfigureAwait(false); if (repos == null) @@ -202,9 +206,14 @@ namespace Tgstation.Server.Host.Controllers { Id = Instance.Id }; - databaseContext.Instances.Attach(instance); - if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false)) - await databaseContext.Save(ct).ConfigureAwait(false); + await databaseContextFactory.UseContext( + async databaseContext => + { + databaseContext.Instances.Attach(instance); + if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false)) + await databaseContext.Save(ct).ConfigureAwait(false); + }) + .ConfigureAwait(false); }, cancellationToken).ConfigureAwait(false); api.Origin = model.Origin; @@ -246,7 +255,7 @@ namespace Tgstation.Server.Host.Controllers Instance = Instance }; var api = currentModel.ToApi(); - await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false); api.ActiveJob = job.ToApi(); return Accepted(api); } @@ -435,7 +444,8 @@ namespace Tgstation.Server.Host.Controllers CancelRight = (ulong)RepositoryRights.CancelPendingChanges, }; - await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) => + // Time to access git, do it in a job + await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) => { using var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false); if (repo == null) @@ -475,15 +485,57 @@ namespace Tgstation.Server.Host.Controllers { Id = Instance.Id }; - databaseContext.Instances.Attach(attachedInstance); - await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); + Task CallLoadRevInfo(Models.TestMerge testMergeToAdd = null, string lastOriginCommitSha = null) => databaseContextFactory + .UseContext( + async databaseContext => + { + databaseContext.Instances.Attach(attachedInstance); + var needsUpdate = await LoadRevisionInformation( + repo, + databaseContext, + attachedInstance, + lastOriginCommitSha, + x => lastRevisionInfo = x, + ct) + .ConfigureAwait(false); + + if (testMergeToAdd != null) + { + // rev info may have already loaded the user + var mergedBy = databaseContext.Users.Local.FirstOrDefault(x => x.Id == AuthenticationContext.User.Id); + if (mergedBy == default) + { + mergedBy = new Models.User + { + Id = AuthenticationContext.User.Id + }; + + databaseContext.Users.Attach(mergedBy); + } + + testMergeToAdd.MergedBy = mergedBy; + + lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge + { + TestMerge = testMergeToAdd + }); + lastRevisionInfo.PrimaryTestMerge = testMergeToAdd; + + databaseContext.Users.Attach(testMergeToAdd.MergedBy); + } + + if (needsUpdate) + await databaseContext.Save(cancellationToken).ConfigureAwait(false); + }); + + await CallLoadRevInfo().ConfigureAwait(false); // apply new rev info, tracking applied test merges - async Task UpdateRevInfo() + async Task UpdateRevInfo(Models.TestMerge testMergeToAdd = null) { var last = lastRevisionInfo; - await LoadRevisionInformation(repo, databaseContext, attachedInstance, last.OriginCommitSha, x => lastRevisionInfo = x, ct).ConfigureAwait(false); + await CallLoadRevInfo(testMergeToAdd, last.OriginCommitSha).ConfigureAwait(false); lastRevisionInfo.ActiveTestMerges.AddRange(last.ActiveTestMerges); } @@ -531,7 +583,7 @@ namespace Tgstation.Server.Host.Controllers throw new JobException(ErrorCode.RepoSwappedShaOrReference); await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false); - await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin + await CallLoadRevInfo().ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin } else NextProgressReporter()(100); @@ -542,7 +594,7 @@ namespace Tgstation.Server.Host.Controllers throw new JobException(ErrorCode.RepoReferenceNotTracking); await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false); await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); - await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); + await CallLoadRevInfo().ConfigureAwait(false); // repo head is on origin so force this // will update the db if necessary @@ -559,10 +611,10 @@ namespace Tgstation.Server.Host.Controllers I.PullRequestRevision = null; var gitHubClient = currentModel.AccessToken != null - ? gitHubClientFactory.CreateClient(currentModel.AccessToken) - : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) - ? gitHubClientFactory.CreateClient() - : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken)); + ? gitHubClientFactory.CreateClient(currentModel.AccessToken) + : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) + ? gitHubClientFactory.CreateClient() + : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken)); var repoOwner = repo.GitHubOwner; var repoName = repo.GitHubRepoName; @@ -600,14 +652,20 @@ namespace Tgstation.Server.Host.Controllers if (!cantSearch) { - var dbPull = await databaseContext.RevisionInformations - .Where(x => x.Instance.Id == Instance.Id - && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha - && x.ActiveTestMerges.Count <= model.NewTestMerges.Count - && x.ActiveTestMerges.Count > 0) - .Include(x => x.ActiveTestMerges) - .ThenInclude(x => x.TestMerge) - .ToListAsync(cancellationToken).ConfigureAwait(false); + List dbPull = null; + + await databaseContextFactory.UseContext( + async databaseContext => + dbPull = await databaseContext.RevisionInformations + .Where(x => x.Instance.Id == Instance.Id + && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha + && x.ActiveTestMerges.Count <= model.NewTestMerges.Count + && x.ActiveTestMerges.Count > 0) + .Include(x => x.ActiveTestMerges) + .ThenInclude(x => x.TestMerge) + .ToListAsync(cancellationToken) + .ConfigureAwait(false)) + .ConfigureAwait(false); // split here cause this bit has to be done locally revInfoWereLookingFor = dbPull @@ -619,7 +677,7 @@ namespace Tgstation.Server.Host.Controllers && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)))) .FirstOrDefault(); - if (revInfoWereLookingFor == null && model.NewTestMerges.Count > 1) + if (revInfoWereLookingFor == default && model.NewTestMerges.Count > 1) { // okay try to add at least SOME prs we've seen before var search = model.NewTestMerges.ToList(); @@ -669,20 +727,6 @@ namespace Tgstation.Server.Host.Controllers if (needToApplyRemainingPrs) { - // an invocation of LoadRevisionInformation could have already loaded this user - var contextUser = databaseContext.Users.Local.Where(x => x.Id == AuthenticationContext.User.Id).FirstOrDefault(); - if (contextUser == default) - { - // No reason to call the DB, just attach it - contextUser = new Models.User - { - Id = AuthenticationContext.User.Id - }; - databaseContext.Users.Attach(contextUser); - } - else - Logger.LogTrace("Skipping attaching the user to the database context as it is already loaded!"); - foreach (var I in model.NewTestMerges) { Octokit.PullRequest pr = null; @@ -740,8 +784,7 @@ namespace Tgstation.Server.Host.Controllers ++doneSteps; - var revInfoUpdateTask = UpdateRevInfo(); - + // MergedBy will be set later var tm = new Models.TestMerge { Author = pr?.User.Login ?? errorMessage, @@ -750,18 +793,11 @@ namespace Tgstation.Server.Host.Controllers TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty, Comment = I.Comment, Number = I.Number, - MergedBy = contextUser, PullRequestRevision = I.PullRequestRevision, Url = pr?.HtmlUrl ?? errorMessage }; - await revInfoUpdateTask.ConfigureAwait(false); - - lastRevisionInfo.PrimaryTestMerge = tm; - lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge - { - TestMerge = tm - }); + await UpdateRevInfo(tm).ConfigureAwait(false); } } } @@ -772,8 +808,6 @@ namespace Tgstation.Server.Host.Controllers await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false); await UpdateRevInfo().ConfigureAwait(false); } - - await databaseContext.Save(ct).ConfigureAwait(false); } catch { diff --git a/src/Tgstation.Server.Host/Database/IDatabaseContextFactory.cs b/src/Tgstation.Server.Host/Database/IDatabaseContextFactory.cs index 7d625a86e7..3a9c0f396a 100644 --- a/src/Tgstation.Server.Host/Database/IDatabaseContextFactory.cs +++ b/src/Tgstation.Server.Host/Database/IDatabaseContextFactory.cs @@ -6,7 +6,7 @@ namespace Tgstation.Server.Host.Database /// /// Factory for scoping usage of s. Meant for use by /// - interface IDatabaseContextFactory + public interface IDatabaseContextFactory { /// /// Run an in the scope of an diff --git a/src/Tgstation.Server.Host/Jobs/IJobManager.cs b/src/Tgstation.Server.Host/Jobs/IJobManager.cs index 2f701be883..a6d3610a9d 100644 --- a/src/Tgstation.Server.Host/Jobs/IJobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/IJobManager.cs @@ -23,10 +23,10 @@ namespace Tgstation.Server.Host.Jobs /// Registers a given and begins running it /// /// The - /// The operation to run taking the started , a , progress reporter and a + /// The operation to run taking the started , a , progress reporter and a /// The for the operation /// A representing a running operation - Task RegisterOperation(Job job, Func, CancellationToken, Task> operation, CancellationToken cancellationToken); + Task RegisterOperation(Job job, Func, CancellationToken, Task> operation, CancellationToken cancellationToken); /// /// Wait for a given to complete diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobManager.cs index 20e97c3e1d..c071c946ed 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobManager.cs @@ -75,50 +75,56 @@ namespace Tgstation.Server.Host.Jobs /// The operation for the /// The for the operation /// A representing the running operation - async Task RunJob(Job job, Func operation, CancellationToken cancellationToken) + async Task RunJob(Job job, Func operation, CancellationToken cancellationToken) { try { + void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails); + try + { + var oldJob = job; + job = new Job { Id = oldJob.Id }; + + await operation(job, databaseContextFactory, cancellationToken).ConfigureAwait(false); + + logger.LogDebug("Job {0} completed!", job.Id); + } + catch (OperationCanceledException) + { + logger.LogDebug("Job {0} cancelled!", job.Id); + job.Cancelled = true; + } + catch (JobException e) + { + job.ErrorCode = e.ErrorCode; + job.ExceptionDetails = e.Message; + LogRegularException(); + if (e.InnerException != null) + logger.LogDebug( + "Inner exception for job {0}: {1}", + job.Id, + e.InnerException is JobException + ? e.InnerException.Message + : e.InnerException.ToString()); + } + catch (Exception e) + { + job.ExceptionDetails = e.ToString(); + LogRegularException(); + } + await databaseContextFactory.UseContext(async databaseContext => { - void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails); - try + var attachedJob = new Job { - var oldJob = job; - job = new Job { Id = oldJob.Id }; - databaseContext.Jobs.Attach(job); + Id = job.Id + }; - await operation(job, databaseContext, cancellationToken).ConfigureAwait(false); - - logger.LogDebug("Job {0} completed!", job.Id); - } - catch (OperationCanceledException) - { - logger.LogDebug("Job {0} cancelled!", job.Id); - job.Cancelled = true; - } - catch (JobException e) - { - job.ErrorCode = e.ErrorCode; - job.ExceptionDetails = e.Message; - LogRegularException(); - if (e.InnerException != null) - logger.LogDebug( - "Inner exception for job {0}: {1}", - job.Id, - e.InnerException is JobException - ? e.InnerException.Message - : e.InnerException.ToString()); - } - catch (Exception e) - { - job.ExceptionDetails = e.ToString(); - LogRegularException(); - } - finally - { - job.StoppedAt = DateTimeOffset.Now; - } + databaseContext.Jobs.Attach(attachedJob); + attachedJob.StoppedAt = DateTimeOffset.Now; + attachedJob.ExceptionDetails = job.ExceptionDetails; + attachedJob.ErrorCode = job.ErrorCode; + attachedJob.Cancelled = job.Cancelled; await databaseContext.Save(default).ConfigureAwait(false); }).ConfigureAwait(false); @@ -135,7 +141,7 @@ namespace Tgstation.Server.Host.Jobs } /// - public Task RegisterOperation(Job job, Func, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext => + public Task RegisterOperation(Job job, Func, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext => { if (job == null) throw new ArgumentNullException(nameof(job));