Jobs now take an IDatabaseContextFactory

As opposed to an IDatabaseContext
This commit is contained in:
Jordan Brown
2020-05-20 23:28:45 -04:00
parent 572d698ec9
commit 960d86ba22
13 changed files with 409 additions and 338 deletions
@@ -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<int> 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);
@@ -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 <see cref="Jobs.IJobManager"/>.
/// </summary>
/// <param name="job">The running <see cref="Job"/>.</param>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the operation.</param>
/// <param name="databaseContextFactory">The <see cref="IDatabaseContextFactory"/> 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 DeploymentProcess(
Job job,
IDatabaseContext databaseContext,
IDatabaseContextFactory databaseContextFactory,
Action<int> progressReporter,
CancellationToken cancellationToken);
}
+127 -119
View File
@@ -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<int> 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<RevisionInformation> 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<RevInfoTestMerge>(oldRevInfo.ActiveTestMerges);
const int NumSteps = 3;
var doneSteps = 0;
databaseContext.Instances.Attach(attachedInstance);
databaseContext.RevisionInformations.Add(currentRevInfo);
hasDbChanges = true;
}
Action<int> 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<RevisionInformation> 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<RevInfoTestMerge>(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);
@@ -211,8 +211,7 @@ namespace Tgstation.Server.Host.Components
repoIoManager,
eventConsumer,
loggerFactory.CreateLogger<Repository.Repository>(),
loggerFactory.CreateLogger<RepositoryManager>(),
metadata.RepositorySettings);
loggerFactory.CreateLogger<RepositoryManager>());
try
{
var byond = new ByondManager(byondIOManager, byondInstaller, eventConsumer, loggerFactory.CreateLogger<ByondManager>());
@@ -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
/// </summary>
readonly ILogger<RepositoryManager> logger;
/// <summary>
/// The <see cref="RepositorySettings"/> for the <see cref="RepositoryManager"/>
/// </summary>
readonly RepositorySettings repositorySettings;
/// <summary>
/// Used for controlling single access to the <see cref="IRepository"/>
/// </summary>
@@ -67,15 +63,13 @@ namespace Tgstation.Server.Host.Components.Repository
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/></param>
/// <param name="repositoryLogger">The value of <see cref="repositoryLogger"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
/// <param name="repositorySettings">The value of <see cref="repositorySettings"/></param>
public RepositoryManager(
ILibGit2RepositoryFactory repositoryFactory,
ILibGit2Commands commands,
IIOManager ioManager,
IEventConsumer eventConsumer,
ILogger<Repository> repositoryLogger,
ILogger<RepositoryManager> logger,
RepositorySettings repositorySettings)
ILogger<RepositoryManager> 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;
}
}
/// <inheritdoc />
@@ -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);
@@ -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();
}
@@ -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());
}
}
@@ -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();
}
@@ -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<Models.RevisionInformation> 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
{
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Host.Database
/// <summary>
/// Factory for scoping usage of <see cref="IDatabaseContext"/>s. Meant for use by <see cref="Components"/>
/// </summary>
interface IDatabaseContextFactory
public interface IDatabaseContextFactory
{
/// <summary>
/// Run an <paramref name="operation"/> in the scope of an <see cref="IDatabaseContext"/>
@@ -23,10 +23,10 @@ namespace Tgstation.Server.Host.Jobs
/// Registers a given <see cref="Job"/> and begins running it
/// </summary>
/// <param name="job">The <see cref="Job"/></param>
/// <param name="operation">The operation to run taking the started <see cref="Job"/>, a <see cref="IDatabaseContext"/>, progress reporter <see cref="Action{T1}"/> and a <see cref="CancellationToken"/></param>
/// <param name="operation">The operation to run taking the started <see cref="Job"/>, a <see cref="IDatabaseContextFactory"/>, progress reporter <see cref="Action{T1}"/> and a <see cref="CancellationToken"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing a running operation</returns>
Task RegisterOperation(Job job, Func<Job, IDatabaseContext, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken);
Task RegisterOperation(Job job, Func<Job, IDatabaseContextFactory, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken);
/// <summary>
/// Wait for a given <paramref name="job"/> to complete
+44 -38
View File
@@ -75,50 +75,56 @@ namespace Tgstation.Server.Host.Jobs
/// <param name="operation">The operation for the <paramref name="job"/></param>
/// <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, IDatabaseContext, CancellationToken, Task> operation, CancellationToken cancellationToken)
async Task RunJob(Job job, Func<Job, IDatabaseContextFactory, CancellationToken, Task> 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
}
/// <inheritdoc />
public Task RegisterOperation(Job job, Func<Job, IDatabaseContext, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
public Task RegisterOperation(Job job, Func<Job, IDatabaseContextFactory, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
{
if (job == null)
throw new ArgumentNullException(nameof(job));