mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-30 00:22:40 +01:00
Merge pull request #652 from tgstation/651-SlowYourRoll
Overhaul of CompileJob database insertion and consumption
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user