Rewrite IDmbProvider locking so that locks are tracked and reasons are given

This commit is contained in:
Jordan Dominion
2024-07-12 18:06:32 -04:00
parent 7d3eed888e
commit 21a8f7cbe1
15 changed files with 428 additions and 214 deletions
@@ -137,7 +137,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
var compileJobToUse = watchdog.ActiveCompileJob;
if (hasStaged)
{
var latestCompileJob = compileJobProvider.LatestCompileJob();
var latestCompileJob = await compileJobProvider.LatestCompileJob();
if (latestCompileJob?.Id != compileJobToUse?.Id)
compileJobToUse = latestCompileJob;
else
@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components.Deployment
{
/// <summary>
/// Manages locks on a given <see cref="IDmbProvider"/>.
/// </summary>
sealed class DeploymentLockManager : IAsyncDisposable
{
/// <summary>
/// The <see cref="Models.CompileJob"/> represented by the <see cref="DeploymentLockManager"/>.
/// </summary>
public CompileJob CompileJob => dmbProvider.CompileJob;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="DeploymentLockManager"/>.
/// </summary>
readonly ILogger logger;
/// <summary>
/// The <see cref="IDmbProvider"/> the <see cref="DeploymentLockManager"/> is managing.
/// </summary>
readonly IDmbProvider dmbProvider;
/// <summary>
/// The <see cref="DmbLock"/>s on the <see cref="dmbProvider"/>.
/// </summary>
readonly HashSet<DmbLock> locks;
/// <summary>
/// The first lock acquired by the <see cref="DeploymentLockManager"/>.
/// </summary>
readonly DmbLock firstLock;
/// <summary>
/// Create a <see cref="DeploymentLockManager"/>.
/// </summary>
/// <param name="dmbProvider">The value of <see cref="dmbProvider"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="initialLockReason">The reason for the first lock.</param>
/// <param name="firstLock">The <see cref="IDmbProvider"/> that represents the first lock.</param>
/// <param name="callerFile">The file path of the calling function.</param>
/// <param name="callerLine">The line number of the call invocation.</param>
/// <returns>A new <see cref="DeploymentLockManager"/>.</returns>
public static DeploymentLockManager Create(IDmbProvider dmbProvider, ILogger logger, string initialLockReason, out IDmbProvider firstLock, [CallerFilePath] string? callerFile = null, [CallerLineNumber] int callerLine = default)
{
var manager = new DeploymentLockManager(dmbProvider, logger, initialLockReason, callerFile!, callerLine);
firstLock = manager.firstLock;
return manager;
}
/// <summary>
/// Generates a verbose description of a given <paramref name="dmbLock"/>.
/// </summary>
/// <param name="dmbLock">The <see cref="DmbLock"/> to get a description of.</param>
/// <returns>A verbose description of <paramref name="dmbLock"/>.</returns>
static string GetFullLockDescriptor(DmbLock dmbLock) => $"{dmbLock.LockID} {dmbLock.Descriptor} (Created at {dmbLock.LockTime}){(dmbLock.KeptAlive ? " (RELEASED)" : String.Empty)}";
/// <summary>
/// Initializes a new instance of the <see cref="DeploymentLockManager"/> class.
/// </summary>
/// <param name="dmbProvider">The value of <see cref="dmbProvider"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="initialLockReason">The reason for the first lock.</param>
/// <param name="callerFile">The file path of the calling function.</param>
/// <param name="callerLine">The line number of the call invocation.</param>
/// <returns>A new <see cref="DeploymentLockManager"/>.</returns>
DeploymentLockManager(IDmbProvider dmbProvider, ILogger logger, string initialLockReason, string callerFile, int callerLine)
{
this.dmbProvider = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
logger.LogTrace("Initializing lock manager for compile job {id}", dmbProvider.CompileJob.Id);
locks = new HashSet<DmbLock>();
firstLock = CreateLock(initialLockReason, callerFile, callerLine);
}
/// <inheritdoc />
public ValueTask DisposeAsync()
=> firstLock.DisposeAsync();
/// <summary>
/// Add a lock to the managed <see cref="IDmbProvider"/>.
/// </summary>
/// <param name="reason">The reason for the lock.</param>
/// <param name="callerFile">The file path of the calling function.</param>
/// <param name="callerLine">The line number of the call invocation.</param>
/// <returns>A <see cref="IDmbProvider"/> whose lifetime represents the lock.</returns>
public IDmbProvider AddLock(string reason, [CallerFilePath] string? callerFile = null, [CallerLineNumber]int callerLine = default)
{
lock (locks)
{
if (locks.Count == 0)
throw new InvalidOperationException($"No locks exist on the DmbProvider for CompileJob {dmbProvider.CompileJob.Id}!");
return CreateLock(reason, callerFile!, callerLine);
}
}
/// <summary>
/// Creates a <see cref="DmbLock"/> and adds it to <see cref="locks"/>.
/// </summary>
/// <param name="reason">The reason for the lock.</param>
/// <param name="callerFile">The file path of the calling function.</param>
/// <param name="callerLine">The line number of the call invocation.</param>
/// <returns>A new <see cref="DmbLock"/>.</returns>
/// <remarks>Requires exclusive write access to <see cref="locks"/> be held by the caller.</remarks>
DmbLock CreateLock(string reason, string callerFile, int callerLine)
{
DmbLock? newLock = null;
string? descriptor = null;
ValueTask LockCleanupAction()
{
ValueTask disposeTask = ValueTask.CompletedTask;
lock (locks)
{
logger.LogTrace("Removing .dmb Lock: {descriptor}", descriptor);
if (locks.Remove(newLock!))
logger.LogTrace("Lock was removed from list successfully");
else
logger.LogError("A .dmb lock was disposed more than once: {descriptor}", descriptor);
if (locks.Count == 0)
disposeTask = dmbProvider.DisposeAsync();
else if (newLock == firstLock)
logger.LogDebug("First lock on CompileJob #{compileJobId} removed, it must cleanup {remaining} remaining locks to be cleaned", CompileJob.Id, locks.Count);
}
return disposeTask;
}
newLock = new DmbLock(LockCleanupAction, dmbProvider, $"{callerFile}#{callerLine}: {reason}");
locks.Add(newLock);
descriptor = GetFullLockDescriptor(newLock!);
logger.LogTrace("Created .dmb Lock: {descriptor}", descriptor);
return newLock;
}
}
}
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@@ -31,14 +31,14 @@ namespace Tgstation.Server.Host.Components.Deployment
{
get
{
lock (jobLockCounts)
lock (jobLockManagers)
return newerDmbTcs.Task;
}
}
/// <inheritdoc />
[MemberNotNullWhen(true, nameof(nextDmbProvider))]
public bool DmbAvailable => nextDmbProvider != null;
[MemberNotNullWhen(true, nameof(nextLockManager))]
public bool DmbAvailable => nextLockManager != null;
/// <summary>
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="DmbFactory"/>.
@@ -78,7 +78,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Map of <see cref="CompileJob.JobId"/>s to locks on them.
/// </summary>
readonly Dictionary<long, int> jobLockCounts;
readonly Dictionary<long, DeploymentLockManager> jobLockManagers;
/// <summary>
/// <see cref="TaskCompletionSource"/> resulting in the latest <see cref="DmbProvider"/> yet to exist.
@@ -91,9 +91,9 @@ namespace Tgstation.Server.Host.Components.Deployment
Task cleanupTask;
/// <summary>
/// The latest <see cref="DmbProvider"/>.
/// The <see cref="DeploymentLockManager"/> for the latest <see cref="DmbProvider"/>.
/// </summary>
IDmbProvider? nextDmbProvider;
DeploymentLockManager? nextLockManager;
/// <summary>
/// If the <see cref="DmbFactory"/> is "started" via <see cref="IComponentService"/>.
@@ -127,7 +127,7 @@ namespace Tgstation.Server.Host.Components.Deployment
cleanupTask = Task.CompletedTask;
newerDmbTcs = new TaskCompletionSource();
cleanupCts = new CancellationTokenSource();
jobLockCounts = new Dictionary<long, int>();
jobLockManagers = new Dictionary<long, DeploymentLockManager>();
}
/// <inheritdoc />
@@ -138,10 +138,13 @@ namespace Tgstation.Server.Host.Components.Deployment
{
ArgumentNullException.ThrowIfNull(job);
var newProvider = await FromCompileJob(job, cancellationToken);
if (newProvider == null)
var (dmbProvider, lockManager) = await FromCompileJobInternal(job, "Compile job loading", cancellationToken);
if (dmbProvider == null)
return;
if (lockManager == null)
throw new InvalidOperationException($"We did not acquire the first lock for compile job {job.Id}!");
// Do this first, because it's entirely possible when we set the tcs it will immediately need to be applied
if (started)
{
@@ -149,16 +152,16 @@ namespace Tgstation.Server.Host.Components.Deployment
metadata,
job);
await remoteDeploymentManager.StageDeployment(
newProvider.CompileJob,
lockManager.CompileJob,
activationAction,
cancellationToken);
}
ValueTask dmbDisposeTask;
lock (jobLockCounts)
lock (jobLockManagers)
{
dmbDisposeTask = nextDmbProvider?.DisposeAsync() ?? ValueTask.CompletedTask;
nextDmbProvider = newProvider;
dmbDisposeTask = nextLockManager?.DisposeAsync() ?? ValueTask.CompletedTask;
nextLockManager = lockManager;
// Oh god dammit
var temp = Interlocked.Exchange(ref newerDmbTcs, new TaskCompletionSource());
@@ -169,20 +172,12 @@ namespace Tgstation.Server.Host.Components.Deployment
}
/// <inheritdoc />
public IDmbProvider LockNextDmb(int lockCount)
public IDmbProvider LockNextDmb(string reason, [CallerFilePath] string? callerFile = null, [CallerLineNumber] int callerLine = default)
{
if (!DmbAvailable)
throw new InvalidOperationException("No .dmb available!");
if (lockCount < 0)
throw new ArgumentOutOfRangeException(nameof(lockCount), lockCount, "lockCount must be greater than or equal to 0!");
lock (jobLockCounts)
{
var jobId = nextDmbProvider.CompileJob.Require(x => x.Id);
var incremented = jobLockCounts[jobId] += lockCount;
logger.LogTrace("Compile job {jobId} lock increased by: {increment}", jobId, lockCount);
LogLockCounts();
return nextDmbProvider;
}
return nextLockManager.AddLock(reason, callerFile, callerLine);
}
/// <inheritdoc />
@@ -217,8 +212,8 @@ namespace Tgstation.Server.Host.Components.Deployment
{
try
{
lock (jobLockCounts)
remoteDeploymentManagerFactory.ForgetLocalStateForCompileJobs(jobLockCounts.Keys);
lock (jobLockManagers)
remoteDeploymentManagerFactory.ForgetLocalStateForCompileJobs(jobLockManagers.Keys);
using (cancellationToken.Register(() => cleanupCts.Cancel()))
await cleanupTask;
@@ -231,124 +226,15 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <inheritdoc />
#pragma warning disable CA1506 // TODO: Decomplexify
public async ValueTask<IDmbProvider?> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
public async ValueTask<IDmbProvider?> FromCompileJob(CompileJob compileJob, string reason, CancellationToken cancellationToken, [CallerFilePath] string? callerFile = null, [CallerLineNumber] int callerLine = default)
{
ArgumentNullException.ThrowIfNull(compileJob);
ArgumentNullException.ThrowIfNull(reason);
// ensure we have the entire metadata tree
var compileJobId = compileJob.Require(x => x.Id);
logger.LogTrace("Loading compile job {id}...", compileJobId);
await databaseContextFactory.UseContext(
async db => compileJob = await db
.CompileJobs
.AsQueryable()
.Where(x => x!.Id == compileJobId)
.Include(x => x.Job!)
.ThenInclude(x => x.StartedBy)
.Include(x => x.Job!)
.ThenInclude(x => x.Instance)
.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)
.FirstAsync(cancellationToken)); // can't wait to see that query
var (dmb, _) = await FromCompileJobInternal(compileJob, reason, cancellationToken, callerFile, callerLine);
EngineVersion engineVersion;
if (!EngineVersion.TryParse(compileJob.EngineVersion, out var engineVersionNullable))
{
logger.LogWarning("Error loading compile job, bad engine version: {engineVersion}", compileJob.EngineVersion);
return null; // omae wa mou shinderu
}
else
engineVersion = engineVersionNullable!;
if (!compileJob.Job.StoppedAt.HasValue)
{
// This happens when we're told to load the compile job that is currently finished up
// It constitutes an API violation if it's returned by the DreamDaemonController so just set it here
// Bit of a hack, but it works out to be nearly if not the same value that's put in the DB
logger.LogTrace("Setting missing StoppedAt for CompileJob.Job #{id}...", compileJob.Job.Id);
compileJob.Job.StoppedAt = DateTimeOffset.UtcNow;
}
var providerSubmitted = false;
void CleanupAction()
{
if (providerSubmitted)
CleanRegisteredCompileJob(compileJob);
}
var newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction));
try
{
const string LegacyADirectoryName = "A";
const string LegacyBDirectoryName = "B";
var dmbExistsAtRoot = await ioManager.FileExists(
ioManager.ConcatPath(
newProvider.Directory,
newProvider.DmbName),
cancellationToken);
if (!dmbExistsAtRoot)
{
logger.LogTrace("Didn't find .dmb at game directory root, checking A/B dirs...");
var primaryCheckTask = ioManager.FileExists(
ioManager.ConcatPath(
newProvider.Directory,
LegacyADirectoryName,
newProvider.DmbName),
cancellationToken);
var secondaryCheckTask = ioManager.FileExists(
ioManager.ConcatPath(
newProvider.Directory,
LegacyBDirectoryName,
newProvider.DmbName),
cancellationToken);
if (!(await primaryCheckTask && await secondaryCheckTask))
{
logger.LogWarning("Error loading compile job, .dmb missing!");
return null; // omae wa mou shinderu
}
// rebuild the provider because it's using the legacy style directories
// Don't dispose it
logger.LogDebug("Creating legacy two folder .dmb provider targeting {aDirName} directory...", LegacyADirectoryName);
newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction), Path.DirectorySeparatorChar + LegacyADirectoryName);
}
lock (jobLockCounts)
{
if (!jobLockCounts.TryGetValue(compileJobId, out int value))
{
value = 1;
logger.LogTrace("Initializing lock count for compile job {id}", compileJobId);
jobLockCounts.Add(compileJobId, 1);
}
else
{
logger.LogTrace("FromCompileJob already had a jobLockCounts entry for {id}. Incrementing lock count to {value}.", compileJobId, value);
jobLockCounts[compileJobId] = ++value;
}
providerSubmitted = true;
LogLockCounts();
return newProvider;
}
}
finally
{
if (!providerSubmitted)
await newProvider.DisposeAsync();
}
return dmb;
}
#pragma warning restore CA1506
/// <inheritdoc />
#pragma warning disable CA1506 // TODO: Decomplexify
@@ -357,8 +243,8 @@ namespace Tgstation.Server.Host.Components.Deployment
List<long> jobIdsToSkip;
// don't clean locked directories
lock (jobLockCounts)
jobIdsToSkip = jobLockCounts.Keys.ToList();
lock (jobLockManagers)
jobIdsToSkip = jobLockManagers.Keys.ToList();
List<string>? jobUidsToNotErase = null;
@@ -413,11 +299,143 @@ namespace Tgstation.Server.Host.Components.Deployment
#pragma warning restore CA1506
/// <inheritdoc />
public CompileJob? LatestCompileJob()
public async ValueTask<CompileJob?> LatestCompileJob()
{
if (!DmbAvailable)
return null;
return LockNextDmb(0)?.CompileJob;
await using IDmbProvider provider = LockNextDmb("Checking latest CompileJob");
return provider.CompileJob;
}
/// <summary>
/// Gets a <see cref="IDmbProvider"/> and potentially the <see cref="DeploymentLockManager"/> for a given <see cref="CompileJob"/>.
/// </summary>
/// <param name="compileJob">The <see cref="CompileJob"/> to make the <see cref="IDmbProvider"/> for.</param>
/// <param name="reason">The reason the compile job needed to be loaded.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <param name="callerFile">The file path of the calling function.</param>
/// <param name="callerLine">The line number of the call invocation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in, on success, a tuple containing new <see cref="IDmbProvider"/> representing the <see cref="CompileJob"/>. If the first lock on <paramref name="compileJob"/> was acquired, the <see cref="DeploymentLockManager"/> will also be returned. On failure, <see langword="null"/> Will be returned.</returns>
async ValueTask<(IDmbProvider? DmbProvider, DeploymentLockManager? LockManager)> FromCompileJobInternal(CompileJob compileJob, string reason, CancellationToken cancellationToken, [CallerFilePath] string? callerFile = null, [CallerLineNumber] int callerLine = default)
{
// ensure we have the entire metadata tree
var compileJobId = compileJob.Require(x => x.Id);
lock (jobLockManagers)
if (jobLockManagers.TryGetValue(compileJobId, out var lockManager))
return (DmbProvider: lockManager.AddLock(reason, callerFile, callerLine), LockManager: null); // fast path
logger.LogTrace("Loading compile job {id}...", compileJobId);
await databaseContextFactory.UseContext(
async db => compileJob = await db
.CompileJobs
.AsQueryable()
.Where(x => x!.Id == compileJobId)
.Include(x => x.Job!)
.ThenInclude(x => x.StartedBy)
.Include(x => x.Job!)
.ThenInclude(x => x.Instance)
.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)
.FirstAsync(cancellationToken)); // can't wait to see that query
EngineVersion engineVersion;
if (!EngineVersion.TryParse(compileJob.EngineVersion, out var engineVersionNullable))
{
logger.LogWarning("Error loading compile job, bad engine version: {engineVersion}", compileJob.EngineVersion);
return (null, null); // omae wa mou shinderu
}
else
engineVersion = engineVersionNullable!;
if (!compileJob.Job.StoppedAt.HasValue)
{
// This happens when we're told to load the compile job that is currently finished up
// It constitutes an API violation if it's returned by the DreamDaemonController so just set it here
// Bit of a hack, but it works out to be nearly if not the same value that's put in the DB
logger.LogTrace("Setting missing StoppedAt for CompileJob.Job #{id}...", compileJob.Job.Id);
compileJob.Job.StoppedAt = DateTimeOffset.UtcNow;
}
var providerSubmitted = false;
void CleanupAction()
{
if (providerSubmitted)
CleanRegisteredCompileJob(compileJob);
}
var newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction));
try
{
const string LegacyADirectoryName = "A";
const string LegacyBDirectoryName = "B";
var dmbExistsAtRoot = await ioManager.FileExists(
ioManager.ConcatPath(
newProvider.Directory,
newProvider.DmbName),
cancellationToken);
if (!dmbExistsAtRoot)
{
logger.LogTrace("Didn't find .dmb at game directory root, checking A/B dirs...");
var primaryCheckTask = ioManager.FileExists(
ioManager.ConcatPath(
newProvider.Directory,
LegacyADirectoryName,
newProvider.DmbName),
cancellationToken);
var secondaryCheckTask = ioManager.FileExists(
ioManager.ConcatPath(
newProvider.Directory,
LegacyBDirectoryName,
newProvider.DmbName),
cancellationToken);
if (!(await primaryCheckTask && await secondaryCheckTask))
{
logger.LogWarning("Error loading compile job, .dmb missing!");
return (null, null); // omae wa mou shinderu
}
// rebuild the provider because it's using the legacy style directories
// Don't dispose it
logger.LogDebug("Creating legacy two folder .dmb provider targeting {aDirName} directory...", LegacyADirectoryName);
#pragma warning disable CA2000 // Dispose objects before losing scope (false positive)
newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction), Path.DirectorySeparatorChar + LegacyADirectoryName);
#pragma warning restore CA2000 // Dispose objects before losing scope
}
lock (jobLockManagers)
{
IDmbProvider lockedProvider;
if (!jobLockManagers.TryGetValue(compileJobId, out var lockManager))
{
lockManager = DeploymentLockManager.Create(newProvider, logger, reason, out lockedProvider);
jobLockManagers.Add(compileJobId, lockManager);
providerSubmitted = true;
}
else
{
lockedProvider = lockManager.AddLock(reason, callerFile, callerLine); // race condition
lockManager = null;
}
return (DmbProvider: lockedProvider, LockManager: lockManager);
}
}
finally
{
if (!providerSubmitted)
await newProvider.DisposeAsync();
}
}
/// <summary>
@@ -428,6 +446,9 @@ namespace Tgstation.Server.Host.Components.Deployment
{
Task HandleCleanup()
{
lock (jobLockManagers)
jobLockManagers.Remove(job.Require(x => x.Id));
var otherTask = cleanupTask;
async Task WrapThrowableTasks()
@@ -453,26 +474,8 @@ namespace Tgstation.Server.Host.Components.Deployment
return Task.WhenAll(otherTask, WrapThrowableTasks());
}
lock (jobLockCounts)
{
var jobId = job.Require(x => x.Id);
if (jobLockCounts.TryGetValue(jobId, out var currentVal))
if (currentVal == 1)
{
jobLockCounts.Remove(jobId);
logger.LogDebug("Cleaning lock-free compile job {id} => {dirName}", jobId, job.DirectoryName);
cleanupTask = HandleCleanup();
}
else
{
var decremented = --jobLockCounts[jobId];
logger.LogTrace("Compile job {id} lock count now: {lockCount}", jobId, decremented);
}
else
logger.LogError("Extra Dispose of DmbProvider for CompileJob {compileJobId}!", jobId);
LogLockCounts();
}
lock (cleanupCts)
cleanupTask = HandleCleanup();
}
/// <summary>
@@ -487,30 +490,5 @@ namespace Tgstation.Server.Host.Components.Deployment
await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List<string> { ioManager.ResolvePath(directory) }, true, cancellationToken);
await ioManager.DeleteDirectory(directory, cancellationToken);
}
/// <summary>
/// Log out the current lock counts to Trace.
/// </summary>
/// <remarks><see cref="jobLockCounts"/> must be locked before calling this function.</remarks>
void LogLockCounts()
{
if (jobLockCounts.Count == 0)
{
logger.LogWarning("No compile jobs registered!");
return;
}
var builder = new StringBuilder();
foreach (var jobId in jobLockCounts.Keys)
{
builder.AppendLine();
builder.Append("\t- ");
builder.Append(jobId);
builder.Append(": ");
builder.Append(jobLockCounts[jobId]);
}
logger.LogTrace("Compile Job Lock Counts:{details}", builder.ToString());
}
}
}
@@ -0,0 +1,82 @@
using System;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components.Deployment
{
/// <summary>
/// Represents a lock on a given <see cref="IDmbProvider"/>.
/// </summary>
sealed class DmbLock : IDmbProvider
{
/// <inheritdoc />
public string DmbName => baseProvider.DmbName;
/// <inheritdoc />
public string Directory => baseProvider.Directory;
/// <inheritdoc />
public CompileJob CompileJob => baseProvider.CompileJob;
/// <inheritdoc />
public EngineVersion EngineVersion => baseProvider.EngineVersion;
/// <summary>
/// Unique ID of the lock.
/// </summary>
public Guid LockID { get; }
/// <summary>
/// The <see cref="DateTimeOffset"/> of when the lock was acquired.
/// </summary>
public DateTimeOffset LockTime { get; }
/// <summary>
/// A description of the <see cref="DmbLock"/>'s purpose.
/// </summary>
public string Descriptor { get; }
/// <summary>
/// If <see cref="KeepAlive"/> was called on the <see cref="DmbLock"/>.
/// </summary>
public bool KeptAlive { get; private set; }
/// <summary>
/// The <see cref="IDmbProvider"/> being wrapped.
/// </summary>
readonly IDmbProvider baseProvider;
/// <summary>
/// A <see cref="Func{TResult}"/> to use as the implementation of <see cref="DisposeAsync"/>.
/// </summary>
readonly Func<ValueTask> disposeAction;
/// <summary>
/// Initializes a new instance of the <see cref="DmbLock"/> class.
/// </summary>
/// <param name="disposeAction">The value of <see cref="disposeAction"/>.</param>
/// <param name="baseProvider">The value of <see cref="baseProvider"/>.</param>
/// <param name="descriptor">The value of <see cref="Descriptor"/>.</param>
public DmbLock(Func<ValueTask> disposeAction, IDmbProvider baseProvider, string descriptor)
{
this.disposeAction = disposeAction ?? throw new ArgumentNullException(nameof(disposeAction));
this.baseProvider = baseProvider ?? throw new ArgumentNullException(nameof(baseProvider));
Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor));
LockID = Guid.NewGuid();
LockTime = DateTimeOffset.UtcNow;
}
/// <inheritdoc />
public ValueTask DisposeAsync() => disposeAction();
/// <inheritdoc />
public void KeepAlive()
{
KeptAlive = true;
baseProvider.KeepAlive();
}
}
}
@@ -326,7 +326,7 @@ namespace Tgstation.Server.Host.Components.Deployment
likelyPushedTestMergeCommit,
cancellationToken);
var activeCompileJob = compileJobConsumer.LatestCompileJob();
var activeCompileJob = await compileJobConsumer.LatestCompileJob();
try
{
await databaseContextFactory.UseContext(
@@ -1,4 +1,5 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@@ -25,17 +26,22 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Gets the next <see cref="IDmbProvider"/>. <see cref="DmbAvailable"/> is a precondition.
/// </summary>
/// <param name="lockCount">The amount of locks to give the resulting <see cref="IDmbProvider"/>. It's <see cref="IDisposable.Dispose"/> must be called this many times to properly clean the job.</param>
/// <param name="reason">The reason the lock is being acquired.</param>
/// <param name="callerFile">The file path of the calling function.</param>
/// <param name="callerLine">The line number of the call invocation.</param>
/// <returns>A new <see cref="IDmbProvider"/>.</returns>
IDmbProvider LockNextDmb(int lockCount);
IDmbProvider LockNextDmb(string reason, [CallerFilePath] string? callerFile = null, [CallerLineNumber] int callerLine = default);
/// <summary>
/// Gets a <see cref="IDmbProvider"/> for a given <see cref="CompileJob"/>.
/// </summary>
/// <param name="compileJob">The <see cref="CompileJob"/> to make the <see cref="IDmbProvider"/> for.</param>
/// <param name="reason">The reason the compile job needed to be loaded.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <param name="callerFile">The file path of the calling function.</param>
/// <param name="callerLine">The line number of the call invocation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IDmbProvider"/> representing the <see cref="CompileJob"/> on success, <see langword="null"/> on failure.</returns>
ValueTask<IDmbProvider?> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken);
ValueTask<IDmbProvider?> FromCompileJob(CompileJob compileJob, string reason, CancellationToken cancellationToken, [CallerFilePath] string? callerFile = null, [CallerLineNumber] int callerLine = default);
/// <summary>
/// Deletes all compile jobs that are inactive in the Game folder.
@@ -1,4 +1,6 @@
using Tgstation.Server.Host.Models;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components.Deployment
{
@@ -10,7 +12,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Gets the latest <see cref="CompileJob"/>.
/// </summary>
/// <returns>The latest <see cref="CompileJob"/> or <see langword="null"/> if none are available.</returns>
CompileJob? LatestCompileJob();
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the latest <see cref="CompileJob"/> or <see langword="null"/> if none are available.</returns>
ValueTask<CompileJob?> LatestCompileJob();
}
}
@@ -258,7 +258,7 @@ namespace Tgstation.Server.Host.Components
}
/// <inheritdoc />
public CompileJob? LatestCompileJob() => dmbFactory.LatestCompileJob();
public ValueTask<CompileJob?> LatestCompileJob() => dmbFactory.LatestCompileJob();
/// <summary>
/// The <see cref="JobEntrypoint"/> for updating the repository.
@@ -576,7 +576,7 @@ namespace Tgstation.Server.Host.Components
continue;
}
if (deploySha == LatestCompileJob()?.RevisionInformation.CommitSha)
if (deploySha == (await LatestCompileJob())?.RevisionInformation.CommitSha)
{
logger.LogTrace("Aborting auto update, same revision as latest CompileJob");
continue;
@@ -58,6 +58,6 @@ namespace Tgstation.Server.Host.Components
public ValueTask ScheduleAutoUpdate(uint newInterval, string? newCron) => Instance.ScheduleAutoUpdate(newInterval, newCron);
/// <inheritdoc />
public CompileJob? LatestCompileJob() => Instance.LatestCompileJob();
public ValueTask<CompileJob?> LatestCompileJob() => Instance.LatestCompileJob();
}
}
@@ -208,7 +208,7 @@ namespace Tgstation.Server.Host.Components.Session
return null;
}
var dmb = await dmbFactory.FromCompileJob(result!.CompileJob!, cancellationToken);
var dmb = await dmbFactory.FromCompileJob(result!.CompileJob!, "Session Loading Main Deployment", cancellationToken);
if (dmb == null)
{
logger.LogError("Unable to reattach! Could not load .dmb!");
@@ -230,7 +230,7 @@ namespace Tgstation.Server.Host.Components.Session
if (result.InitialCompileJob != null)
{
logger.LogTrace("Loading initial compile job...");
initialDmb = await dmbFactory.FromCompileJob(result.InitialCompileJob, cancellationToken);
initialDmb = await dmbFactory.FromCompileJob(result.InitialCompileJob, "Session Loading Initial Deployment", cancellationToken);
}
logger.LogTrace("Retrieved ReattachInformation");
@@ -232,7 +232,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <inheritdoc />
protected sealed override async ValueTask HandleNewDmbAvailable(CancellationToken cancellationToken)
{
IDmbProvider compileJobProvider = DmbFactory.LockNextDmb(1);
IDmbProvider compileJobProvider = DmbFactory.LockNextDmb("AdvancedWatchdog next compile job preload");
bool canSeamlesslySwap = CanUseSwappableDmbProvider(compileJobProvider);
if (canSeamlesslySwap)
if (compileJobProvider.CompileJob.EngineVersion != ActiveCompileJob!.EngineVersion)
@@ -214,7 +214,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
{
// don't need a new dmb if reattaching
var reattachInProgress = reattachInfo != null;
var dmbToUse = reattachInProgress ? null : DmbFactory.LockNextDmb(1);
var dmbToUse = reattachInProgress ? null : DmbFactory.LockNextDmb("Watchdog initialization");
// if this try catches something, both servers are killed
try
@@ -815,21 +815,18 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
/// <param name="currentCompileJob">The session's current <see cref="CompileJob"/>.</param>
/// <returns>A <see cref="Task"/> that completes if and when a newer <see cref="CompileJob"/> is available.</returns>
Task InitialCheckDmbUpdated(CompileJob currentCompileJob)
async Task InitialCheckDmbUpdated(CompileJob currentCompileJob)
{
var factoryTask = DmbFactory.OnNewerDmb;
var latestCompileJob = DmbFactory.LatestCompileJob();
if (latestCompileJob == null)
return factoryTask;
if (latestCompileJob.Id != currentCompileJob.Id)
var latestCompileJob = await DmbFactory.LatestCompileJob();
if (latestCompileJob != null && latestCompileJob.Id != currentCompileJob.Id)
{
Logger.LogDebug("Found new CompileJob without waiting");
return Task.CompletedTask;
return;
}
return factoryTask;
await factoryTask;
}
/// <summary>
@@ -87,7 +87,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
return;
}
Server.ReattachInformation.InitialDmb = await DmbFactory.FromCompileJob(Server.CompileJob, cancellationToken);
Server.ReattachInformation.InitialDmb = await DmbFactory.FromCompileJob(Server.CompileJob, "WindowsWatchdog Initial Deployment", cancellationToken);
}
/// <inheritdoc />
@@ -386,7 +386,7 @@ namespace Tgstation.Server.Host.Controllers
if (revision)
{
var latestCompileJob = instance.LatestCompileJob();
var latestCompileJob = await instance.LatestCompileJob();
result.ActiveCompileJob = ((instance.Watchdog.Status != WatchdogStatus.Offline
? dd.ActiveCompileJob
: latestCompileJob) ?? latestCompileJob)