From a0d583b5961d353d24024e7e200283cdae799a8a Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sat, 21 Oct 2023 18:01:41 -0400 Subject: [PATCH] New hard link system for PosixWatchdog - IDmbProvider is now `AsyncDisposable` as opposed to `IDisposable`. - Add hard link support to `ISymlinkFactory`. - `PosixWatchdog` now has to mirror deployment structure as hard links before swapping. Because of this deployments may not immediately be applied if the reboots immediately after they were completed. --- .../Components/Deployment/DmbFactory.cs | 7 +- .../Components/Deployment/DmbProvider.cs | 9 +- .../Components/Deployment/DreamMaker.cs | 2 +- .../Deployment/HardLinkDmbProvider.cs | 222 ++++++++++++++++++ .../Components/Deployment/IDmbProvider.cs | 2 +- .../Deployment/SwappableDmbProvider.cs | 62 +++-- .../Deployment/TemporaryDmbProvider.cs | 5 +- .../Components/Session/ISessionController.cs | 4 +- .../Components/Session/SessionController.cs | 11 +- .../Components/Watchdog/BasicWatchdog.cs | 2 +- .../Components/Watchdog/PosixWatchdog.cs | 21 +- .../Watchdog/PosixWatchdogFactory.cs | 1 + .../Components/Watchdog/WindowsWatchdog.cs | 67 ++++-- .../IO/ISymlinkFactory.cs | 9 + .../IO/PosixSymlinkFactory.cs | 16 ++ .../IO/WindowsSymlinkFactory.cs | 4 + 16 files changed, 380 insertions(+), 64 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index 09567ba79c..df587cab11 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -148,15 +148,18 @@ namespace Tgstation.Server.Host.Components.Deployment cancellationToken); } + ValueTask dmbDisposeTask; lock (jobLockCounts) { - nextDmbProvider?.Dispose(); + dmbDisposeTask = nextDmbProvider?.DisposeAsync() ?? ValueTask.CompletedTask; nextDmbProvider = newProvider; // Oh god dammit var temp = Interlocked.Exchange(ref newerDmbTcs, new TaskCompletionSource()); temp.SetResult(); } + + await dmbDisposeTask; } /// @@ -320,7 +323,7 @@ namespace Tgstation.Server.Host.Components.Deployment finally { if (!providerSubmitted) - newProvider.Dispose(); + await newProvider.DisposeAsync(); } } #pragma warning restore CA1506 diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs index f3711ede4f..770409f790 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Models; @@ -30,7 +31,7 @@ namespace Tgstation.Server.Host.Components.Deployment readonly string directoryAppend; /// - /// The to run when is called. + /// The to run when is called. /// Action onDispose; @@ -50,7 +51,11 @@ namespace Tgstation.Server.Host.Components.Deployment } /// - public void Dispose() => onDispose?.Invoke(); + public ValueTask DisposeAsync() + { + onDispose?.Invoke(); + return ValueTask.CompletedTask; + } /// public void KeepAlive() => onDispose = null; diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 665efba795..307a1bb107 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -802,7 +802,7 @@ namespace Tgstation.Server.Host.Components.Deployment job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider ApiValidationStatus validationStatus; - using (var provider = new TemporaryDmbProvider(ioManager.ResolvePath(job.DirectoryName.ToString()), String.Concat(job.DmeName, DmbExtension), job)) + await using (var provider = new TemporaryDmbProvider(ioManager.ResolvePath(job.DirectoryName.ToString()), String.Concat(job.DmeName, DmbExtension), job)) await using (var controller = await sessionControllerFactory.LaunchNew(provider, byondLock, launchParameters, true, cancellationToken)) { var launchResult = await controller.LaunchResult.WaitAsync(cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs new file mode 100644 index 0000000000..e5ddede026 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Utils; + +namespace Tgstation.Server.Host.Components.Deployment +{ + /// + /// A that uses hard links. + /// + sealed class HardLinkDmbProvider : SwappableDmbProvider + { + /// + /// The for . + /// + readonly CancellationTokenSource cancellationTokenSource; + + /// + /// The representing the base provider mirroring operation. + /// + readonly Task mirroringTask; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The for the . + /// The for the . + /// The for the . + /// The value of . + /// The for the . + public HardLinkDmbProvider( + IDmbProvider baseProvider, + IIOManager ioManager, + ISymlinkFactory symlinkFactory, + ILogger logger, + GeneralConfiguration generalConfiguration) + : base( + baseProvider, + ioManager, + symlinkFactory) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + cancellationTokenSource = new CancellationTokenSource(); + try + { + mirroringTask = MirrorSourceDirectory(generalConfiguration.GetCopyDirectoryTaskThrottle(), cancellationTokenSource.Token); + } + catch + { + cancellationTokenSource.Dispose(); + throw; + } + } + + /// + public override async ValueTask DisposeAsync() + { + cancellationTokenSource.Cancel(); + cancellationTokenSource.Dispose(); + try + { + await mirroringTask; + } + catch (OperationCanceledException ex) + { + logger.LogDebug(ex, "Mirroring task cancelled!"); + } + + await base.DisposeAsync(); + } + + /// + public override Task FinishActivationPreparation(CancellationToken cancellationToken) + { + if (!mirroringTask.IsCompleted) + logger.LogTrace("Waiting for mirroring to complete..."); + + return mirroringTask.WaitAsync(cancellationToken); + } + + /// + protected override async Task DoSwap(CancellationToken cancellationToken) + { + var mirroredDir = await mirroringTask.WaitAsync(cancellationToken); + var goAheadTcs = new TaskCompletionSource(); + + // I feel dirty... + async void DisposeOfOldDirectory() + { + var directoryMoved = false; + var disposePath = Guid.NewGuid().ToString(); + try + { + await IOManager.MoveDirectory(LiveGameDirectory, disposePath, cancellationToken); + directoryMoved = true; + goAheadTcs.SetResult(); + await IOManager.DeleteDirectory(disposePath, CancellationToken.None); // DCT: We're detached at this point + } + catch (Exception ex) + { + if (directoryMoved) + logger.LogWarning(ex, "Failed to delete hard linked directory: {disposePath}", disposePath); + else + { + logger.LogDebug(ex, "Live directory appears to not exist"); + goAheadTcs.SetResult(); + } + } + } + + DisposeOfOldDirectory(); + await goAheadTcs.Task; + await IOManager.MoveDirectory(mirroredDir, LiveGameDirectory, cancellationToken); + } + + /// + /// Mirror the . + /// + /// The optional maximum number of simultaneous tasks allowed to execute. + /// The for the operation. + /// A resulting in the full path to the mirrored directory. + async Task MirrorSourceDirectory(int? taskThrottle, CancellationToken cancellationToken) + { + var stopwatch = Stopwatch.StartNew(); + var mirrorGuid = Guid.NewGuid(); + logger.LogDebug("Starting to mirror {sourceDir} as hard links to {mirrorGuid}...", CompileJob.DirectoryName, mirrorGuid); + if (taskThrottle.HasValue && taskThrottle < 1) + throw new ArgumentOutOfRangeException(nameof(taskThrottle), taskThrottle, "taskThrottle must be at least 1!"); + + var src = IOManager.ResolvePath(CompileJob.DirectoryName.ToString()); + var dest = IOManager.ResolvePath(mirrorGuid.ToString()); + + using var semaphore = taskThrottle.HasValue ? new SemaphoreSlim(taskThrottle.Value) : null; + await Task.WhenAll(MirrorDirectoryImpl(src, dest, semaphore, cancellationToken)); + stopwatch.Stop(); + + logger.LogDebug( + "Finished mirror of {sourceDir} to {mirrorGuid} in {seconds}s...", + CompileJob.DirectoryName, + mirrorGuid, + stopwatch.Elapsed.TotalSeconds.ToString("0.##", CultureInfo.InvariantCulture)); + + return dest; + } + + /// + /// Recursively create tasks to create a hard link directory mirror of to . + /// + /// The source directory path. + /// The destination directory path. + /// Optional used to limit degree of parallelism. + /// The for the operation. + /// A of s representing the running operations. The first returned is always the necessary call to . + /// I genuinely don't know how this will work with symlinked files. Waiting for the issue report I guess. + IEnumerable MirrorDirectoryImpl(string src, string dest, SemaphoreSlim semaphore, CancellationToken cancellationToken) + { + var dir = new DirectoryInfo(src); + Task subdirCreationTask = null; + foreach (var subDirectory in dir.EnumerateDirectories()) + { + // check if we are a symbolic link + if (!subDirectory.Attributes.HasFlag(FileAttributes.Directory) || subDirectory.Attributes.HasFlag(FileAttributes.ReparsePoint)) + { + logger.LogTrace("Skipping symlink to {subdir}", subDirectory.Name); + continue; + } + + var checkingSubdirCreationTask = true; + foreach (var copyTask in MirrorDirectoryImpl(subDirectory.FullName, Path.Combine(dest, subDirectory.Name), semaphore, cancellationToken)) + { + if (subdirCreationTask == null) + { + subdirCreationTask = copyTask; + yield return subdirCreationTask; + } + else if (!checkingSubdirCreationTask) + yield return copyTask; + + checkingSubdirCreationTask = false; + } + } + + foreach (var fileInfo in dir.EnumerateFiles()) + { + if (subdirCreationTask == null) + { + subdirCreationTask = IOManager.CreateDirectory(dest, cancellationToken); + yield return subdirCreationTask; + } + + var sourceFile = fileInfo.FullName; + var destFile = IOManager.ConcatPath(dest, fileInfo.Name); + + async Task LinkThisFile() + { + await subdirCreationTask.WaitAsync(cancellationToken); + using var lockContext = semaphore != null + ? await SemaphoreSlimContext.Lock(semaphore, cancellationToken) + : null; + await SymlinkFactory.CreateHardLink(sourceFile, destFile, cancellationToken); + } + + yield return LinkThisFile(); + } + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs index efc773a919..c1c477ffff 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs @@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// Provides absolute paths to the latest compiled .dmbs. /// - public interface IDmbProvider : IDisposable + public interface IDmbProvider : IAsyncDisposable { /// /// The file name of the .dmb. diff --git a/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs index e496453222..14cdc268e2 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs @@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// A that uses symlinks. /// - sealed class SwappableDmbProvider : IDmbProvider + class SwappableDmbProvider : IDmbProvider { /// /// The directory where the is symlinked to. @@ -21,7 +21,7 @@ namespace Tgstation.Server.Host.Components.Deployment public string DmbName => baseProvider.DmbName; /// - public string Directory => ioManager.ResolvePath(LiveGameDirectory); + public string Directory => IOManager.ResolvePath(LiveGameDirectory); /// public CompileJob CompileJob => baseProvider.CompileJob; @@ -31,20 +31,20 @@ namespace Tgstation.Server.Host.Components.Deployment /// public bool Swapped => swapped != 0; - /// - /// The we are swapping for. - /// - readonly IDmbProvider baseProvider; - /// /// The to use. /// - readonly IIOManager ioManager; + protected IIOManager IOManager { get; } /// /// The to use. /// - readonly ISymlinkFactory symlinkFactory; + protected ISymlinkFactory SymlinkFactory { get; } + + /// + /// The we are swapping for. + /// + readonly IDmbProvider baseProvider; /// /// Backing field for . @@ -55,17 +55,17 @@ namespace Tgstation.Server.Host.Components.Deployment /// Initializes a new instance of the class. /// /// The value of . - /// The value of . - /// The value of . + /// The value of . + /// The value of . public SwappableDmbProvider(IDmbProvider baseProvider, IIOManager ioManager, ISymlinkFactory symlinkFactory) { this.baseProvider = baseProvider ?? throw new ArgumentNullException(nameof(baseProvider)); - this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); - this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory)); + IOManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + SymlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory)); } /// - public void Dispose() => baseProvider.Dispose(); + public virtual ValueTask DisposeAsync() => baseProvider.DisposeAsync(); /// public void KeepAlive() => baseProvider.KeepAlive(); @@ -75,19 +75,37 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// The for the operation. /// A representing the running operation. - public async Task MakeActive(CancellationToken cancellationToken) + public Task MakeActive(CancellationToken cancellationToken) { if (Interlocked.Exchange(ref swapped, 1) != 0) throw new InvalidOperationException("Already swapped!"); - if (symlinkFactory.SymlinkedDirectoriesAreDeletedAsFiles) - await ioManager.DeleteFile(LiveGameDirectory, cancellationToken); - else - await ioManager.DeleteDirectory(LiveGameDirectory, cancellationToken); + return DoSwap(cancellationToken); + } - await symlinkFactory.CreateSymbolicLink( - ioManager.ResolvePath(baseProvider.Directory), - ioManager.ResolvePath(LiveGameDirectory), + /// + /// Should be . before calling to ensure the is ready to instantly swap. Can be called multiple times. + /// + /// The for the operation. + /// A representing the preparation process. + public virtual Task FinishActivationPreparation(CancellationToken cancellationToken) + => Task.CompletedTask; + + /// + /// Perform the swapping action. + /// + /// The for the operation. + /// A representing the running operation. + protected virtual async Task DoSwap(CancellationToken cancellationToken) + { + if (SymlinkFactory.SymlinkedDirectoriesAreDeletedAsFiles) + await IOManager.DeleteFile(LiveGameDirectory, cancellationToken); + else + await IOManager.DeleteDirectory(LiveGameDirectory, cancellationToken); + + await SymlinkFactory.CreateSymbolicLink( + IOManager.ResolvePath(baseProvider.Directory), + IOManager.ResolvePath(LiveGameDirectory), cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/TemporaryDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/TemporaryDmbProvider.cs index 0bf982cfa3..4476d9d2c3 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/TemporaryDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/TemporaryDmbProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Tgstation.Server.Host.Models; @@ -32,9 +33,7 @@ namespace Tgstation.Server.Host.Components.Deployment } /// - public void Dispose() - { - } + public ValueTask DisposeAsync() => ValueTask.CompletedTask; /// public void KeepAlive() => throw new NotSupportedException(); diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs index 3ded84429a..93f10eb17c 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs @@ -128,7 +128,7 @@ namespace Tgstation.Server.Host.Components.Session /// Replace the in use with a given , disposing the old one. /// /// The new . - /// An to be disposed once certain that the original is no longer in use. - IDisposable ReplaceDmbProvider(IDmbProvider newProvider); + /// An to be disposed once certain that the original is no longer in use. + IAsyncDisposable ReplaceDmbProvider(IDmbProvider newProvider); } } diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 2da8c03b58..eab32bdce1 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -350,8 +350,13 @@ namespace Tgstation.Server.Host.Components.Session await process.DisposeAsync(); byondLock.Dispose(); bridgeRegistration?.Dispose(); - ReattachInformation.Dmb.Dispose(); - ReattachInformation.InitialDmb?.Dispose(); + var regularDmbDisposeTask = ReattachInformation.Dmb.DisposeAsync(); + var initialDmb = ReattachInformation.InitialDmb; + if (initialDmb != null) + await initialDmb.DisposeAsync(); + + await regularDmbDisposeTask; + chatTrackingContext.Dispose(); reattachTopicCts.Dispose(); @@ -552,7 +557,7 @@ namespace Tgstation.Server.Host.Components.Session public void Resume() => process.Resume(); /// - public IDisposable ReplaceDmbProvider(IDmbProvider dmbProvider) + public IAsyncDisposable ReplaceDmbProvider(IDmbProvider dmbProvider) { var oldDmb = ReattachInformation.Dmb; ReattachInformation.Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider)); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 8396ce7c99..fc5c3461b0 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -268,7 +268,7 @@ namespace Tgstation.Server.Host.Components.Watchdog // server didn't get control of this dmb if (dmbToUse != null && !serverWasActive) - dmbToUse.Dispose(); + await dmbToUse.DisposeAsync(); throw; } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs index 044ef3f1ef..3bf2927e57 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -9,6 +10,7 @@ using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Session; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; @@ -21,6 +23,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// sealed class PosixWatchdog : WindowsWatchdog { + /// + /// The for the . + /// + readonly GeneralConfiguration generalConfiguration; + /// /// Initializes a new instance of the class. /// @@ -39,6 +46,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The for the . + /// The value of . /// The autostart value for the . public PosixWatchdog( IChatManager chat, @@ -56,6 +64,7 @@ namespace Tgstation.Server.Host.Components.Watchdog ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, + GeneralConfiguration generalConfiguration, bool autoStart) : base( chat, @@ -75,13 +84,15 @@ namespace Tgstation.Server.Host.Components.Watchdog instance, autoStart) { + this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration)); } /// protected override Task ApplyInitialDmb(CancellationToken cancellationToken) - { - // not necessary to hold initial .dmb on Linux because of based inode deletes - return Task.CompletedTask; - } + => Task.CompletedTask; // not necessary to hold initial .dmb on Linux because of based inode deletes + + /// + protected override SwappableDmbProvider CreateSwappableDmbProvider(IDmbProvider dmbProvider) + => new HardLinkDmbProvider(dmbProvider, GameIOManager, SymlinkFactory, Logger, generalConfiguration); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs index 6774c73cb5..787127a05c 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs @@ -76,6 +76,7 @@ namespace Tgstation.Server.Host.Components.Watchdog LoggerFactory.CreateLogger(), settings, instance, + GeneralConfiguration, settings.AutoStart ?? throw new ArgumentNullException(nameof(settings))); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index c8d6c7eb44..0e42c3b660 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// The for the . /// - readonly ISymlinkFactory symlinkFactory; + protected ISymlinkFactory SymlinkFactory { get; } /// /// of s that are waiting to clean up old deployments. @@ -68,7 +68,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The value of . - /// The value of . + /// The value of . /// The for the . /// The for the . /// The for the . @@ -109,7 +109,7 @@ namespace Tgstation.Server.Host.Components.Watchdog try { GameIOManager = gameIOManager ?? throw new ArgumentNullException(nameof(gameIOManager)); - this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory)); + SymlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory)); deploymentCleanupTasks = new List(); } @@ -131,7 +131,7 @@ namespace Tgstation.Server.Host.Components.Watchdog // If we reach this point, we can guarantee PrepServerForLaunch will be called before starting again. ActiveSwappable = null; - pendingSwappable?.Dispose(); + await (pendingSwappable?.DisposeAsync() ?? ValueTask.CompletedTask); pendingSwappable = null; await DrainDeploymentCleanupTasks(true); @@ -142,9 +142,10 @@ namespace Tgstation.Server.Host.Components.Watchdog { if (pendingSwappable != null) { - var updateTask = BeforeApplyDmb(pendingSwappable.CompileJob, cancellationToken); + Task RunPrequel() => BeforeApplyDmb(pendingSwappable.CompileJob, cancellationToken); - if (!pendingSwappable.Swapped) + var needToSwap = !pendingSwappable.Swapped; + if (needToSwap) { // IMPORTANT: THE SESSIONCONTROLLER SHOULD STILL BE PROCESSING THE BRIDGE REQUEST SO WE KNOW DD IS SLEEPING // OTHERWISE, IT COULD RETURN TO /world/Reboot() TOO EARLY AND LOAD THE WRONG .DMB @@ -153,18 +154,29 @@ namespace Tgstation.Server.Host.Components.Watchdog // integration test logging will catch this Logger.LogError( "The reboot bridge request completed before the watchdog could suspend the server! This can lead to buggy DreamDaemon behaviour and should be reported! To ensure stability, we will need to hard reboot the server"); - await updateTask; + await RunPrequel(); return MonitorAction.Restart; } - await PerformDmbSwap(pendingSwappable, cancellationToken); + // DCT: Not necessary + if (!pendingSwappable.FinishActivationPreparation(CancellationToken.None).IsCompleted) + { + // rare pokemon + Logger.LogInformation("Deployed .dme is not ready to swap, delaying until next reboot!"); + Chat.QueueWatchdogMessage("The pending deployment was not ready to be activated this reboot. It will be applied at the next one."); + return MonitorAction.Continue; + } } + var updateTask = RunPrequel(); + if (needToSwap) + await PerformDmbSwap(pendingSwappable, cancellationToken); + var currentCompileJobId = Server.ReattachInformation.Dmb.CompileJob.Id; await DrainDeploymentCleanupTasks(false); - IDisposable lingeringDeployment; + IAsyncDisposable lingeringDeployment; var localDeploymentCleanupGate = new TaskCompletionSource(); async Task CleanupLingeringDeployment() { @@ -191,7 +203,7 @@ namespace Tgstation.Server.Host.Components.Watchdog ? " due to timeout!" : "..."); - lingeringDeployment.Dispose(); + await lingeringDeployment.DisposeAsync(); } var oldDeploymentCleanupGate = Interlocked.Exchange(ref deploymentCleanupGate, localDeploymentCleanupGate); @@ -247,31 +259,31 @@ namespace Tgstation.Server.Host.Components.Watchdog if (!canSeamlesslySwap) { - compileJobProvider.Dispose(); + await compileJobProvider.DisposeAsync(); await base.HandleNewDmbAvailable(cancellationToken); return; } - SwappableDmbProvider windowsProvider = null; + SwappableDmbProvider swappableProvider = null; try { - windowsProvider = new SwappableDmbProvider(compileJobProvider, GameIOManager, symlinkFactory); + swappableProvider = CreateSwappableDmbProvider(compileJobProvider); if (ActiveCompileJob.DMApiVersion == null) { Logger.LogWarning("Active compile job has no DMAPI! Commencing immediate .dmb swap. Note this behavior is known to be buggy in some DM code contexts. See https://github.com/tgstation/tgstation-server/issues/1550"); - await PerformDmbSwap(windowsProvider, cancellationToken); + await PerformDmbSwap(swappableProvider, cancellationToken); } } catch (Exception ex) { Logger.LogError(ex, "Exception while swapping"); - IDmbProvider providerToDispose = windowsProvider ?? compileJobProvider; - providerToDispose.Dispose(); + IDmbProvider providerToDispose = swappableProvider ?? compileJobProvider; + await providerToDispose.DisposeAsync(); throw; } - pendingSwappable?.Dispose(); - pendingSwappable = windowsProvider; + await (pendingSwappable?.DisposeAsync() ?? ValueTask.CompletedTask); + pendingSwappable = swappableProvider; } /// @@ -284,7 +296,7 @@ namespace Tgstation.Server.Host.Components.Watchdog Logger.LogTrace("Prep for server launch"); - ActiveSwappable = new SwappableDmbProvider(dmbToUse, GameIOManager, symlinkFactory); + ActiveSwappable = CreateSwappableDmbProvider(dmbToUse); try { await InitialLink(cancellationToken); @@ -310,6 +322,14 @@ namespace Tgstation.Server.Host.Components.Watchdog Server.ReattachInformation.InitialDmb = await DmbFactory.FromCompileJob(Server.CompileJob, cancellationToken); } + /// + /// Create a for a given . + /// + /// The to create a for. + /// A new . + protected virtual SwappableDmbProvider CreateSwappableDmbProvider(IDmbProvider dmbProvider) + => new SwappableDmbProvider(dmbProvider, GameIOManager, SymlinkFactory); + /// protected override async Task SessionStartupPersist(CancellationToken cancellationToken) { @@ -332,10 +352,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// The for the operation. /// A representing the running operation. - Task InitialLink(CancellationToken cancellationToken) + async ValueTask InitialLink(CancellationToken cancellationToken) { - Logger.LogTrace("Symlinking compile job..."); - return ActiveSwappable.MakeActive(cancellationToken); + await ActiveSwappable.FinishActivationPreparation(cancellationToken); + Logger.LogTrace("Linking compile job..."); + await ActiveSwappable.MakeActive(cancellationToken); } /// @@ -348,6 +369,8 @@ namespace Tgstation.Server.Host.Components.Watchdog { Logger.LogDebug("Swapping to compile job {id}...", newProvider.CompileJob.Id); + await newProvider.FinishActivationPreparation(cancellationToken); + var suspended = false; var server = Server; try diff --git a/src/Tgstation.Server.Host/IO/ISymlinkFactory.cs b/src/Tgstation.Server.Host/IO/ISymlinkFactory.cs index 1adbd99d97..08320a3d15 100644 --- a/src/Tgstation.Server.Host/IO/ISymlinkFactory.cs +++ b/src/Tgstation.Server.Host/IO/ISymlinkFactory.cs @@ -22,5 +22,14 @@ namespace Tgstation.Server.Host.IO /// The for the operation. /// A representing the running operation. Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken); + + /// + /// Creates a hard link. + /// + /// The path to the hard target. + /// The path to the link. + /// The for the operation. + /// A representing the running operation. + Task CreateHardLink(string targetPath, string linkPath, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/IO/PosixSymlinkFactory.cs b/src/Tgstation.Server.Host/IO/PosixSymlinkFactory.cs index ddbcbb86fb..05d64a3bf0 100644 --- a/src/Tgstation.Server.Host/IO/PosixSymlinkFactory.cs +++ b/src/Tgstation.Server.Host/IO/PosixSymlinkFactory.cs @@ -15,6 +15,22 @@ namespace Tgstation.Server.Host.IO /// public bool SymlinkedDirectoriesAreDeletedAsFiles => true; + /// + public Task CreateHardLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew( + () => + { + ArgumentNullException.ThrowIfNull(targetPath); + ArgumentNullException.ThrowIfNull(linkPath); + + cancellationToken.ThrowIfCancellationRequested(); + var fsInfo = new UnixFileInfo(targetPath); + cancellationToken.ThrowIfCancellationRequested(); + fsInfo.CreateLink(linkPath); + }, + cancellationToken, + DefaultIOManager.BlockingTaskCreationOptions, + TaskScheduler.Current); + /// public Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew( () => diff --git a/src/Tgstation.Server.Host/IO/WindowsSymlinkFactory.cs b/src/Tgstation.Server.Host/IO/WindowsSymlinkFactory.cs index 18abde6b8f..864242d992 100644 --- a/src/Tgstation.Server.Host/IO/WindowsSymlinkFactory.cs +++ b/src/Tgstation.Server.Host/IO/WindowsSymlinkFactory.cs @@ -16,6 +16,10 @@ namespace Tgstation.Server.Host.IO /// public bool SymlinkedDirectoriesAreDeletedAsFiles => false; + /// + public Task CreateHardLink(string targetPath, string linkPath, CancellationToken cancellationToken) + => throw new NotSupportedException(); + /// public Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew( () =>