Merge pull request #652 from tgstation/651-SlowYourRoll

Overhaul of CompileJob database insertion and consumption
This commit is contained in:
Jordan Brown
2018-09-15 15:32:54 -04:00
committed by GitHub
10 changed files with 254 additions and 162 deletions
@@ -169,7 +169,7 @@ namespace Tgstation.Server.Host.Components.Compiler
public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) =>
{
//where complete clause not necessary, only successful COMPILEjobs get in the db
var cj = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && !x.Job.Cancelled.Value && x.Job.ExceptionDetails == null && x.Job.StoppedAt != null)
var cj = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id)
.Include(x => x.Job).ThenInclude(x => x.StartedBy)
.Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
.Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
@@ -245,7 +245,7 @@ namespace Tgstation.Server.Host.Components.Compiler
//find the uids of locked directories
await databaseContextFactory.UseContext(async db =>
{
jobUidsToNotErase = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id) && x.DirectoryName.HasValue).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false);
jobUidsToNotErase = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id)).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
//add the other exemption
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Components.Byond;
using Tgstation.Server.Host.Components.Chat;
@@ -25,12 +26,7 @@ namespace Tgstation.Server.Host.Components
/// The <see cref="IByondManager"/> for the <see cref="IInstance"/>
/// </summary>
IByondManager ByondManager { get; }
/// <summary>
/// The <see cref="IDreamMaker"/> for the <see cref="IInstance"/>
/// </summary>
IDreamMaker DreamMaker { get; }
/// <summary>
/// The <see cref="IWatchdog"/> for the <see cref="IInstance"/>
/// </summary>
@@ -41,11 +37,6 @@ namespace Tgstation.Server.Host.Components
/// </summary>
IChat Chat { get; }
/// <summary>
/// The <see cref="ICompileJobConsumer"/> for the <see cref="IInstance"/>
/// </summary>
ICompileJobConsumer CompileJobConsumer { get; }
/// <summary>
/// The <see cref="StaticFiles.IConfiguration"/> for the <see cref="IInstance"/>
/// </summary>
@@ -57,12 +48,6 @@ namespace Tgstation.Server.Host.Components
/// <returns>The latest <see cref="CompileJob"/> if it exists</returns>
CompileJob LatestCompileJob();
/// <summary>
/// Get the <see cref="Api.Models.Instance"/> associated with the <see cref="IInstance"/>
/// </summary>
/// <returns>The <see cref="Api.Models.Instance"/> associated with the <see cref="IInstance"/></returns>
Api.Models.Instance GetMetadata();
/// <summary>
/// Rename the <see cref="IInstance"/>
/// </summary>
@@ -75,5 +60,15 @@ namespace Tgstation.Server.Host.Components
/// <param name="newInterval">The new auto update inteval</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task SetAutoUpdateInterval(uint newInterval);
/// <summary>
/// Run the compile job and insert it into the database. Meant to be called by a <see cref="Core.IJobManager"/>
/// </summary>
/// <param name="job">The running <see cref="Job"/></param>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> for the operation</param>
/// <param name="progressReporter">The <see cref="Action{T1}"/> to report compilation progress</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task CompileProcess(Job job, IServiceProvider serviceProvider, Action<int> progressReporter, CancellationToken cancellationToken);
}
}
+135 -55
View File
@@ -1,9 +1,11 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components.Byond;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Compiler;
@@ -48,6 +50,11 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IDmbFactory dmbFactory;
/// <summary>
/// The <see cref="IJobManager"/> for the <see cref="Instance"/>
/// </summary>
readonly IJobManager jobManager;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="Instance"/>
/// </summary>
@@ -80,8 +87,9 @@ namespace Tgstation.Server.Host.Components
/// <param name="compileJobConsumer">The value of <see cref="CompileJobConsumer"/></param>
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
/// <param name="dmbFactory">The value of <see cref="dmbFactory"/></param>
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, ILogger<Instance> logger)
public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, ILogger<Instance> logger)
{
this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
@@ -93,6 +101,7 @@ namespace Tgstation.Server.Host.Components
CompileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -107,6 +116,67 @@ namespace Tgstation.Server.Host.Components
RepositoryManager.Dispose();
}
/// <inheritdoc />
public async Task CompileProcess(Job job, IServiceProvider serviceProvider, Action<int> progressReporter, CancellationToken cancellationToken)
{
//DO NOT FOLLOW THE SUGGESTION FOR A THROW EXPRESSION HERE
if (job == null)
throw new ArgumentNullException(nameof(job));
if (serviceProvider == null)
throw new ArgumentNullException(nameof(serviceProvider));
if (progressReporter == null)
throw new ArgumentNullException(nameof(progressReporter));
var databaseContext = serviceProvider.GetRequiredService<IDatabaseContext>();
var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == metadata.Id).Select(x => new DreamDaemonSettings
{
StartupTimeout = x.StartupTimeout,
SecurityLevel = x.SecurityLevel
}).FirstOrDefaultAsync(cancellationToken);
var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
if (dreamMakerSettings == default)
throw new JobException("Missing DreamMakerSettings in DB!");
var ddSettings = await ddSettingsTask.ConfigureAwait(false);
if (ddSettings == default)
throw new JobException("Missing DreamDaemonSettings in DB!");
CompileJob compileJob;
RevisionInformation revInfo;
using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
{
if (repo == null)
throw new JobException("Missing Repository!");
var repoSha = repo.Head;
revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).FirstOrDefaultAsync().ConfigureAwait(false);
if (revInfo == default)
{
revInfo = new RevisionInformation
{
CommitSha = repoSha,
OriginCommitSha = repoSha,
Instance = new Models.Instance
{
Id = metadata.Id
}
};
logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
databaseContext.Instances.Attach(revInfo.Instance);
}
compileJob = await DreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false);
}
compileJob.Job = job;
databaseContext.CompileJobs.Add(compileJob); //will be saved by job context
job.PostComplete = ct => CompileJobConsumer.LoadCompileJob(compileJob, ct);
}
/// <summary>
/// Pull the repository and compile for every set of given <paramref name="minutes"/>
/// </summary>
@@ -122,86 +192,98 @@ namespace Tgstation.Server.Host.Components
try
{
CompileJob job = null;
//need this the whole time
await databaseContextFactory.UseContext(async (db) =>
Models.User user = null;
await databaseContextFactory.UseContext(async (db) => user = await db.Users.FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
var repositoryUpdateJob = new Job
{
//start up queries we'll need in the future
var instanceQuery = db.Instances.Where(x => x.Id == metadata.Id);
var ddSettingsTask = instanceQuery.Select(x => x.DreamDaemonSettings).Select(x => new DreamDaemonSettings
Instance = new Models.Instance
{
StartupTimeout = x.StartupTimeout,
SecurityLevel = x.SecurityLevel
}).FirstAsync(cancellationToken);
var dmSettingsTask = instanceQuery.Select(x => x.DreamMakerSettings).FirstAsync(cancellationToken);
var repositorySettingsTask = instanceQuery.Select(x => x.RepositorySettings).FirstAsync(cancellationToken);
Id = metadata.Id
},
Description = "Scheduled repository update",
CancelRightsType = RightsType.Repository,
CancelRight = (ulong)RepositoryRights.CancelPendingChanges
};
using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
var noRepo = false;
await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, serviceProvider, progressReporter, jobCancellationToken) =>
{
var db = serviceProvider.GetRequiredService<IDatabaseContext>();
var repositorySettingsTask = db.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken);
//assume 5 steps with synchronize
const int ProgressSections = 5;
const int ProgressStep = 100 / ProgressSections;
progressReporter(0 * ProgressStep);
using (var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false))
{
if (repo == null)
{
//no repo, no auto updates
noRepo = true;
return;
}
progressReporter(1 * ProgressStep);
//start the rev info query
var startSha = repo.Head;
var revInfoTask = instanceQuery.SelectMany(x => x.RevisionInformations).Where(x => x.CommitSha == startSha).FirstOrDefaultAsync(cancellationToken);
//need repo setting to fetch
var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, null, cancellationToken).ConfigureAwait(false);
const int SecondStepProgress = 2 * ProgressStep;
progressReporter(SecondStepProgress);
//the main point of auto update is to pull the remote
await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, x => progressReporter(SecondStepProgress + (x / ProgressSections)), jobCancellationToken).ConfigureAwait(false);
progressReporter(3 * ProgressStep);
var startSha = repo.Head;
//take appropriate auto update actions
bool shouldSyncTracked;
if (repositorySettings.AutoUpdatesKeepTestMerges.Value)
{
var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, cancellationToken).ConfigureAwait(false);
var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, jobCancellationToken).ConfigureAwait(false);
if (!result.HasValue)
return;
shouldSyncTracked = result.Value;
}
else
{
await repo.ResetToOrigin(cancellationToken).ConfigureAwait(false);
await repo.ResetToOrigin(jobCancellationToken).ConfigureAwait(false);
shouldSyncTracked = true;
}
progressReporter(4 * ProgressStep);
//synch if necessary
if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head)
await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, shouldSyncTracked, cancellationToken).ConfigureAwait(false);
await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, shouldSyncTracked, jobCancellationToken).ConfigureAwait(false);
//finish other queries
var dmSettings = await dmSettingsTask.ConfigureAwait(false);
var ddSettings = await ddSettingsTask.ConfigureAwait(false);
var revInfo = await revInfoTask.ConfigureAwait(false);
//null rev info handling
if (revInfo == default)
{
var currentSha = repo.Head;
revInfo = new RevisionInformation
{
CommitSha = currentSha,
OriginCommitSha = currentSha,
Instance = new Models.Instance
{
Id = metadata.Id
}
};
logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentSha);
db.Instances.Attach(revInfo.Instance);
}
//finally start compile
job = await DreamMaker.Compile(revInfo, dmSettings, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false);
progressReporter(5 * ProgressStep);
}
}, cancellationToken).ConfigureAwait(false);
db.CompileJobs.Add(job);
await db.Save(cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false);
await CompileJobConsumer.LoadCompileJob(job, cancellationToken).ConfigureAwait(false);
if (noRepo)
continue;
//finally set up the job
var compileProcessJob = new Job
{
StartedBy = user,
Instance = repositoryUpdateJob.Instance,
Description = "Scheduled code deployment",
CancelRightsType = RightsType.DreamMaker,
CancelRight = (ulong)DreamMakerRights.CancelCompile
};
await jobManager.RegisterOperation(compileProcessJob, CompileProcess, cancellationToken).ConfigureAwait(false);
await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
logger.LogDebug("Cancelled auto update job!");
throw;
}
catch (Exception e)
@@ -214,11 +296,9 @@ namespace Tgstation.Server.Host.Components
{
break;
}
logger.LogTrace("Leaving auto update loop...");
}
/// <inheritdoc />
public Api.Models.Instance GetMetadata() => metadata.CloneMetadata();
/// <inheritdoc />
public void Rename(string newName)
{
@@ -238,7 +318,7 @@ namespace Tgstation.Server.Host.Components
CompileJob latestCompileJob = null;
await databaseContextFactory.UseContext(async db =>
{
latestCompileJob = await db.CompileJobs.Where(x => x.Job.Instance.Id == metadata.Id && x.Job.ExceptionDetails == null).OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
latestCompileJob = await db.CompileJobs.Where(x => x.Job.Instance.Id == metadata.Id).OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
await dmbFactory.CleanUnusedCompileJobs(latestCompileJob, cancellationToken).ConfigureAwait(false);
}
@@ -8,7 +8,6 @@ using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Chat.Commands;
using Tgstation.Server.Host.Components.Compiler;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Components.StaticFiles;
using Tgstation.Server.Host.Components.Watchdog;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.IO;
@@ -89,6 +88,11 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IWatchdogFactory watchdogFactory;
/// <summary>
/// The <see cref="IJobManager"/> for the <see cref="InstanceFactory"/>
/// </summary>
readonly IJobManager jobManager;
/// <summary>
/// Construct an <see cref="InstanceFactory"/>
/// </summary>
@@ -106,7 +110,8 @@ namespace Tgstation.Server.Host.Components
/// <param name="processExecutor">The value of <see cref="processExecutor"/></param>
/// <param name="postWriteHandler">The value of <see cref="postWriteHandler"/></param>
/// <param name="watchdogFactory">The value of <see cref="watchdogFactory"/></param>
public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, IServerControl serverUpdater, ICryptographySuite cryptographySuite, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IProviderFactory providerFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IWatchdogFactory watchdogFactory)
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, IServerControl serverUpdater, ICryptographySuite cryptographySuite, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IProviderFactory providerFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IWatchdogFactory watchdogFactory, IJobManager jobManager)
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
@@ -122,6 +127,7 @@ namespace Tgstation.Server.Host.Components
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
this.watchdogFactory = watchdogFactory ?? throw new ArgumentNullException(nameof(watchdogFactory));
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
}
/// <inheritdoc />
@@ -162,7 +168,7 @@ namespace Tgstation.Server.Host.Components
{
var dreamMaker = new DreamMaker(byond, gameIoManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, chat, processExecutor, watchdog, loggerFactory.CreateLogger<DreamMaker>());
return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory, loggerFactory.CreateLogger<Instance>());
return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory, jobManager, loggerFactory.CreateLogger<Instance>());
}
catch
{
@@ -1,4 +1,5 @@
using Byond.TopicSender;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
@@ -807,11 +808,14 @@ namespace Tgstation.Server.Host.Components.Watchdog
if (!autoStart)
return;
long? adminUserId = null;
await databaseContextFactory.UseContext(async db => adminUserId = await db.Users.Select(x => x.Id).FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
var job = new Models.Job
{
StartedBy = new Models.User
{
Id = 1 //just use admin for this cause whatever
Id = adminUserId.Value
},
Instance = new Models.Instance
{
@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
@@ -10,7 +9,6 @@ using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
@@ -78,7 +76,7 @@ namespace Tgstation.Server.Host.Controllers
[TgsAuthorize(DreamMakerRights.CompileJobs)]
public override async Task<IActionResult> List(CancellationToken cancellationToken)
{
var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StartedAt).Select(x => new Api.Models.CompileJob
var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StoppedAt).Select(x => new Api.Models.CompileJob
{
Id = x.Id
}).ToListAsync(cancellationToken).ConfigureAwait(false);
@@ -97,7 +95,7 @@ namespace Tgstation.Server.Host.Controllers
CancelRight = (ulong)DreamMakerRights.CancelCompile,
Instance = Instance
};
await jobManager.RegisterOperation(job, (paramJob, serviceProvider, progressReporter, ct) => RunCompile(paramJob, serviceProvider, Instance, ct), cancellationToken).ConfigureAwait(false);
await jobManager.RegisterOperation(job, instanceManager.GetInstance(Instance).CompileProcess, cancellationToken).ConfigureAwait(false);
return Accepted(job.ToApi());
}
@@ -129,70 +127,5 @@ namespace Tgstation.Server.Host.Controllers
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
return await Read(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Run the compile job and insert it into the database
/// </summary>
/// <param name="job">The running <see cref="Job"/></param>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> for the operation</param>
/// <param name="instanceModel">The <see cref="Models.Instance"/> for the operation</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task RunCompile(Job job, IServiceProvider serviceProvider, Models.Instance instanceModel, CancellationToken cancellationToken)
{
var instanceManager = serviceProvider.GetRequiredService<IInstanceManager>();
var databaseContext = serviceProvider.GetRequiredService<IDatabaseContext>();
var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == instanceModel.Id).Select(x => new DreamDaemonSettings
{
StartupTimeout = x.StartupTimeout,
SecurityLevel = x.SecurityLevel
}).FirstOrDefaultAsync(cancellationToken);
var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == instanceModel.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
if (dreamMakerSettings == default)
throw new JobException("Missing DreamMakerSettings in DB!");
var ddSettings = await ddSettingsTask.ConfigureAwait(false);
if (ddSettings == default)
throw new JobException("Missing DreamDaemonSettings in DB!");
var instance = instanceManager.GetInstance(instanceModel);
CompileJob compileJob;
RevisionInformation revInfo;
using (var repo = await instance.RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
{
if (repo == null)
throw new JobException("Missing Repository!");
var repoSha = repo.Head;
revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).FirstOrDefaultAsync().ConfigureAwait(false);
if (revInfo == default)
{
revInfo = new RevisionInformation
{
CommitSha = repoSha,
OriginCommitSha = repoSha,
Instance = new Models.Instance
{
Id = Instance.Id
}
};
Logger.LogWarning(Repository.OriginTrackingErrorTemplate, repoSha);
databaseContext.Instances.Attach(revInfo.Instance);
}
compileJob = await instance.DreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false);
}
compileJob.Job = job;
databaseContext.CompileJobs.Add(compileJob);
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
await instance.CompileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -295,6 +295,7 @@ namespace Tgstation.Server.Host.Controllers
}
var originalOnline = originalModel.Online.Value;
var renamed = model.Name != null && originalModel.Name != model.Name;
if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate)
|| CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration)
@@ -311,6 +312,9 @@ namespace Tgstation.Server.Host.Controllers
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
if (renamed)
instanceManager.GetInstance(originalModel).Rename(originalModel.Name);
var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
try
{
@@ -25,6 +25,18 @@ namespace Tgstation.Server.Host.Core
/// <returns>A <see cref="Task"/> representing a running operation</returns>
Task RegisterOperation(Job job, Func<Job, IServiceProvider, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken);
/// <summary>
/// Wait for a given <paramref name="job"/> to complete
/// </summary>
/// <param name="job">The <see cref="Job"/> to wait for </param>
/// <param name="canceller">The <see cref="User"/> to cancel the <paramref name="job"/></param>
/// <param name="jobCancellationToken">A <see cref="CancellationToken"/> that will cancel the <paramref name="job"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the <see cref="Job"/></returns>
#pragma warning disable CA1068 // CancellationToken parameters must come last https://github.com/dotnet/roslyn-analyzers/issues/1816
Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken);
#pragma warning restore CA1068 // CancellationToken parameters must come last
/// <summary>
/// Cancels a give <paramref name="job"/>
/// </summary>
+61 -14
View File
@@ -70,13 +70,35 @@ namespace Tgstation.Server.Host.Core
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task RunJob(Job job, Func<Job, IServiceProvider, CancellationToken, Task> operation, CancellationToken cancellationToken)
{
{
try
{
using (var scope = serviceProvider.CreateScope())
{
async Task HandleExceptions(Task task)
{
try
{
await task.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
logger.LogDebug("Job {0} cancelled!", job.Id);
job.Cancelled = true;
}
catch (Exception e)
{
job.ExceptionDetails = e is JobException ? e.Message : e.ToString();
logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
}
finally
{
job.StoppedAt = DateTimeOffset.Now;
}
}
IDatabaseContext databaseContext = null;
try
async Task RunJobInternal()
{
var oldJob = job;
job = new Job { Id = oldJob.Id };
@@ -86,19 +108,21 @@ namespace Tgstation.Server.Host.Core
await operation(job, scope.ServiceProvider, cancellationToken).ConfigureAwait(false);
logger.LogDebug("Job {0} completed!", job.Id);
}
catch (OperationCanceledException)
{
logger.LogDebug("Job {0} cancelled!", job.Id);
job.Cancelled = true;
}
catch (Exception e)
{
job.ExceptionDetails = e is JobException ? e.Message : e.ToString();
logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
}
job.StoppedAt = DateTimeOffset.Now;
};
await HandleExceptions(RunJobInternal()).ConfigureAwait(false);
await databaseContext.Save(default).ConfigureAwait(false);
bool JobErroredOrCancelled() => job.ExceptionDetails != null || job.Cancelled.Value;
//ok so, now it's time for the post commit step if it exists
if (!JobErroredOrCancelled() && job.PostComplete != null)
{
await HandleExceptions(job.PostComplete(cancellationToken)).ConfigureAwait(false);
if (JobErroredOrCancelled())
await databaseContext.Save(default).ConfigureAwait(false);
}
}
}
finally
@@ -223,6 +247,8 @@ namespace Tgstation.Server.Host.Core
/// <inheritdoc />
public int? JobProgress(Job job)
{
if (job == null)
throw new ArgumentNullException(nameof(job));
lock (this)
{
if (!jobs.TryGetValue(job.Id, out var handler))
@@ -230,5 +256,26 @@ namespace Tgstation.Server.Host.Core
return handler.Progress;
}
}
/// <inheritdoc />
public async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
{
if (job == null)
throw new ArgumentNullException(nameof(job));
if (canceller == null)
throw new ArgumentNullException(nameof(canceller));
JobHandler handler;
lock (this)
{
if (!jobs.TryGetValue(job.Id, out handler))
return;
}
Task cancelTask = null;
using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken)))
await handler.Wait(cancellationToken).ConfigureAwait(false);
if (cancelTask != null)
await cancelTask.ConfigureAwait(false);
}
}
}
+12 -1
View File
@@ -1,4 +1,8 @@
using System.ComponentModel.DataAnnotations;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Models
{
@@ -22,6 +26,13 @@ namespace Tgstation.Server.Host.Models
[Required]
public Instance Instance { get; set; }
/// <summary>
/// A <see cref="Task"/> to run after the job completes. This will not affect the <see cref="Api.Models.Internal.Job.StoppedAt"/> time, unless it is cancelled or errors
/// </summary>
/// <remarks>This should only be used where there are database dependencies that also rely on the Job itself completing A.K.A. manually initiated <see cref="CompileJob"/>s</remarks>
[NotMapped]
public Func<CancellationToken, Task> PostComplete { get; set; }
/// <summary>
/// Convert the <see cref="Job"/> to it's API form
/// </summary>