From b2df9802ccfcb1412380c6fd00405aa4621bfbfa Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 10 Aug 2021 21:53:46 -0400 Subject: [PATCH] Session persistence happens at launch --- .../Components/Session/ISessionController.cs | 9 ++- .../Components/Session/ISessionPersistor.cs | 7 ++ .../Components/Session/SessionController.cs | 61 ++++++++-------- .../Components/Session/SessionPersistor.cs | 70 ++++++++++++++----- .../Components/Watchdog/BasicWatchdog.cs | 5 +- .../Components/Watchdog/PosixWatchdog.cs | 4 +- .../Components/Watchdog/WatchdogBase.cs | 64 ++++++----------- 7 files changed, 124 insertions(+), 96 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs index c06f18044c..343e7eec37 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs @@ -39,6 +39,11 @@ namespace Tgstation.Server.Host.Components.Session /// CompileJob CompileJob { get; } + /// + /// Gets the associated with the . + /// + ReattachInformation ReattachInformation { get; } + /// /// If the port should be rotated off when the world reboots. /// @@ -67,8 +72,8 @@ namespace Tgstation.Server.Host.Components.Session /// /// Releases the without terminating it. Also calls . /// - /// A resulting in which can be used to create a new . - Task Release(); + /// A representing the running operation. + Task Release(); /// /// Sends a command to DreamDaemon through /world/Topic(). diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionPersistor.cs b/src/Tgstation.Server.Host/Components/Session/ISessionPersistor.cs index 4dc2f4c0ab..3028e6daf4 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionPersistor.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionPersistor.cs @@ -22,5 +22,12 @@ namespace Tgstation.Server.Host.Components.Session /// The for the operation. /// A resulting in the stored if any. Task Load(CancellationToken cancellationToken); + + /// + /// Clear any stored . + /// + /// The for the operation. + /// A representing the running operation. + Task Clear(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index a7a5d71478..eb8969db4c 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -27,7 +27,7 @@ namespace Tgstation.Server.Host.Components.Session sealed class SessionController : ISessionController, IBridgeHandler, IChannelSink { /// - public DMApiParameters DMApiParameters => reattachInformation; + public DMApiParameters DMApiParameters => ReattachInformation; /// public ApiValidationStatus ApiValidationStatus @@ -41,10 +41,10 @@ namespace Tgstation.Server.Host.Components.Session } /// - public Models.CompileJob CompileJob => reattachInformation.Dmb.CompileJob; + public Models.CompileJob CompileJob => ReattachInformation.Dmb.CompileJob; /// - public RebootState RebootState => reattachInformation.RebootState; + public RebootState RebootState => ReattachInformation.RebootState; /// public Version DMApiVersion { get; private set; } @@ -68,12 +68,12 @@ namespace Tgstation.Server.Host.Components.Session public Task OnPrime => primeTcs.Task; /// - public bool DMApiAvailable => reattachInformation.Dmb.CompileJob.DMApiVersion?.Major == DMApiConstants.InteropVersion.Major; + public bool DMApiAvailable => ReattachInformation.Dmb.CompileJob.DMApiVersion?.Major == DMApiConstants.InteropVersion.Major; /// - /// The up to date . + /// The up to date . /// - readonly ReattachInformation reattachInformation; + public ReattachInformation ReattachInformation { get; } /// /// The that completes when DD makes it's first bridge request. @@ -173,7 +173,7 @@ namespace Tgstation.Server.Host.Components.Session /// /// Initializes a new instance of the class. /// - /// The value of . + /// The value of . /// The owning . /// The value of . /// The value of . @@ -203,7 +203,7 @@ namespace Tgstation.Server.Host.Components.Session bool reattached, bool apiValidate) { - this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); + ReattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); this.process = process ?? throw new ArgumentNullException(nameof(process)); this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock)); @@ -277,7 +277,7 @@ namespace Tgstation.Server.Host.Components.Session process.Dispose(); bridgeRegistration?.Dispose(); - reattachInformation.Dmb?.Dispose(); // will be null when released + ReattachInformation.Dmb?.Dispose(); // will be null when released chatTrackingContext.Dispose(); reattachTopicCts.Dispose(); @@ -346,13 +346,13 @@ namespace Tgstation.Server.Host.Components.Session var currentPort = parameters.CurrentPort.Value; if (!nextPort.HasValue) - reattachInformation.Port = parameters.CurrentPort.Value; // not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to + ReattachInformation.Port = parameters.CurrentPort.Value; // not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to else { // nextPort is ready, tell DD to switch to that // if it fails it'll kill itself response.NewPort = nextPort.Value; - reattachInformation.Port = nextPort.Value; + ReattachInformation.Port = nextPort.Value; nextPort = null; // we'll also get here from SetPort so complete that task @@ -398,12 +398,12 @@ namespace Tgstation.Server.Host.Components.Session response.RuntimeInformation = new RuntimeInformation( chatTrackingContext, - reattachInformation.Dmb, - reattachInformation.RuntimeInformation.ServerVersion, - reattachInformation.RuntimeInformation.InstanceName, - reattachInformation.RuntimeInformation.SecurityLevel, - reattachInformation.RuntimeInformation.ServerPort, - reattachInformation.RuntimeInformation.ApiValidateOnly); + ReattachInformation.Dmb, + ReattachInformation.RuntimeInformation.ServerVersion, + ReattachInformation.RuntimeInformation.InstanceName, + ReattachInformation.RuntimeInformation.SecurityLevel, + ReattachInformation.RuntimeInformation.ServerPort, + ReattachInformation.RuntimeInformation.ApiValidateOnly); // Load custom commands chatTrackingContext.CustomCommands = parameters.CustomCommands; @@ -436,19 +436,18 @@ namespace Tgstation.Server.Host.Components.Session public void EnableCustomChatCommands() => chatTrackingContext.Active = DMApiAvailable; /// - public async Task Release() + public async Task Release() { CheckDisposed(); // we still don't want to dispose the dmb yet, even though we're keeping it alive - var tmpProvider = reattachInformation.Dmb; - reattachInformation.Dmb = null; + var tmpProvider = ReattachInformation.Dmb; + ReattachInformation.Dmb = null; released = true; await DisposeAsync().ConfigureAwait(false); byondLock.DoNotDeleteThisSession(); tmpProvider.KeepAlive(); - reattachInformation.Dmb = tmpProvider; - return reattachInformation; + ReattachInformation.Dmb = tmpProvider; } /// @@ -471,7 +470,7 @@ namespace Tgstation.Server.Host.Components.Session return null; } - parameters.AccessIdentifier = reattachInformation.AccessIdentifier; + parameters.AccessIdentifier = ReattachInformation.AccessIdentifier; var json = JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings); logger.LogTrace("Topic request: {0}", json); @@ -483,7 +482,7 @@ namespace Tgstation.Server.Host.Components.Session byondTopicSender.SanitizeString(DMApiConstants.TopicData), byondTopicSender.SanitizeString(json)); - var targetPort = reattachInformation.Port; + var targetPort = ReattachInformation.Port; var topicResponse = await byondTopicSender.SendTopic( new IPEndPoint(IPAddress.Loopback, targetPort), @@ -547,7 +546,7 @@ namespace Tgstation.Server.Host.Components.Session if (commandResult.InteropResponse?.ErrorMessage != null) return false; - reattachInformation.Port = port; + ReattachInformation.Port = port; return true; } @@ -572,7 +571,7 @@ namespace Tgstation.Server.Host.Components.Session logger.LogTrace("Changing reboot state to {0}", newRebootState); - reattachInformation.RebootState = newRebootState; + ReattachInformation.RebootState = newRebootState; var result = await SendCommand( new TopicParameters(newRebootState), cancellationToken) @@ -586,7 +585,7 @@ namespace Tgstation.Server.Host.Components.Session { CheckDisposed(); logger.LogTrace("Resetting reboot state..."); - reattachInformation.RebootState = RebootState.Normal; + ReattachInformation.RebootState = RebootState.Normal; } /// @@ -601,15 +600,15 @@ namespace Tgstation.Server.Host.Components.Session /// public void ReplaceDmbProvider(IDmbProvider dmbProvider) { - var oldDmb = reattachInformation.Dmb; - reattachInformation.Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider)); + var oldDmb = ReattachInformation.Dmb; + ReattachInformation.Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider)); oldDmb.Dispose(); } /// public Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken) { - reattachInformation.RuntimeInformation.InstanceName = newInstanceName; + ReattachInformation.RuntimeInformation.InstanceName = newInstanceName; return SendCommand(new TopicParameters(newInstanceName), cancellationToken); } @@ -667,7 +666,7 @@ namespace Tgstation.Server.Host.Components.Session var reattachResponse = await SendCommand( new TopicParameters( assemblyInformationProvider.Version, - reattachInformation.RuntimeInformation.ServerPort), + ReattachInformation.RuntimeInformation.ServerPort), reattachTopicCts.Token) .ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs b/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs index c8cdcb5cd2..e5d8e415d5 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs @@ -72,12 +72,7 @@ namespace Tgstation.Server.Host.Components.Session logger.LogDebug("Saving reattach information: {0}...", reattachInformation); - await db - .ReattachInformations - .AsQueryable() - .Where(x => x.CompileJob.Job.Instance.Id == metadata.Id) - .DeleteAsync(cancellationToken) - .ConfigureAwait(false); + await ClearImpl(db, false, cancellationToken).ConfigureAwait(false); var dbReattachInfo = new Models.ReattachInformation { @@ -129,23 +124,25 @@ namespace Tgstation.Server.Host.Components.Session bool first = true; foreach (var reattachInfo in dbReattachInfos) { - if (!first) + if (first) { - logger.LogWarning("Killing PID {0} associated with extra reattach information...", reattachInfo.ProcessId); - try - { - using var process = processExecutor.GetProcess(reattachInfo.ProcessId); - process.Terminate(); - await process.Lifetime.ConfigureAwait(false); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to kill process!"); - } + first = false; + continue; + } + + logger.LogWarning("Killing PID {0} associated with extra reattach information...", reattachInfo.ProcessId); + try + { + using var process = processExecutor.GetProcess(reattachInfo.ProcessId); + process.Terminate(); + await process.Lifetime.ConfigureAwait(false); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to kill process!"); } db.ReattachInformations.Remove(reattachInfo); - first = false; } await db.Save(cancellationToken).ConfigureAwait(false); @@ -173,5 +170,40 @@ namespace Tgstation.Server.Host.Components.Session return info; } + + /// + public Task Clear(CancellationToken cancellationToken) => databaseContextFactory + .UseContext( + db => + { + logger.LogDebug("Clearing reattach information"); + return ClearImpl(db, true, cancellationToken); + }); + + /// + /// Clear any stored . + /// + /// The to use. + /// If an SQL DELETE WHERE command should be used rather than an Entity Framework transaction. + /// The for the operation. + /// A representing the running operation. + async Task ClearImpl(IDatabaseContext databaseContext, bool instant, CancellationToken cancellationToken) + { + var baseQuery = databaseContext + .ReattachInformations + .AsQueryable() + .Where(x => x.CompileJob.Job.Instance.Id == metadata.Id); + + if (instant) + await baseQuery + .DeleteAsync(cancellationToken) + .ConfigureAwait(false); + else + { + var results = await baseQuery.ToListAsync(cancellationToken).ConfigureAwait(false); + foreach (var result in results) + databaseContext.ReattachInformations.Remove(result); + } + } } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 6cc3fdac2d..ec41aa29f4 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -199,7 +199,7 @@ namespace Tgstation.Server.Host.Components.Watchdog protected sealed override ISessionController GetActiveController() => Server; /// - protected override async Task InitControllers( + protected override async Task InitController( Task chatTask, ReattachInformation reattachInfo, CancellationToken cancellationToken) @@ -249,6 +249,9 @@ namespace Tgstation.Server.Host.Components.Watchdog Server.SetHighPriority(); + if (!reattachInProgress) + await SessionPersistor.Save(Server.ReattachInformation, cancellationToken).ConfigureAwait(false); + await CheckLaunchResult(Server, "Server", cancellationToken).ConfigureAwait(false); Server.EnableCustomChatCommands(); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs index b43c9ccdde..91b9bf82f8 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs @@ -105,11 +105,11 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - protected override async Task InitControllers(Task chatTask, ReattachInformation reattachInfo, CancellationToken cancellationToken) + protected override async Task InitController(Task chatTask, ReattachInformation reattachInfo, CancellationToken cancellationToken) { try { - await base.InitControllers(chatTask, reattachInfo, cancellationToken).ConfigureAwait(false); + await base.InitController(chatTask, reattachInfo, cancellationToken).ConfigureAwait(false); } finally { diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 38f867557d..ad4db13d02 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -60,6 +60,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// protected TaskCompletionSource ActiveParametersUpdated { get; set; } + /// + /// The for the . + /// + protected ISessionPersistor SessionPersistor { get; } + /// /// The for the . /// @@ -105,11 +110,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly SemaphoreSlim controllerDisposeSemaphore; - /// - /// The for the . - /// - readonly ISessionPersistor sessionPersistor; - /// /// The for the . /// @@ -135,11 +135,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly bool autoStart; - /// - /// Used when detaching servers. - /// - ReattachInformation releasedReattachInformation; - /// /// The for the monitor loop. /// @@ -176,7 +171,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of . /// The value of . /// The value of . - /// The value of . + /// The value of . /// The value of . /// The to populate with. /// The value of . @@ -206,7 +201,7 @@ namespace Tgstation.Server.Host.Components.Watchdog Chat = chat ?? throw new ArgumentNullException(nameof(chat)); SessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory)); DmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory)); - this.sessionPersistor = sessionPersistor ?? throw new ArgumentNullException(nameof(sessionPersistor)); + SessionPersistor = sessionPersistor ?? throw new ArgumentNullException(nameof(sessionPersistor)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); AsyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.diagnosticsIOManager = diagnosticsIOManager ?? throw new ArgumentNullException(nameof(diagnosticsIOManager)); @@ -347,7 +342,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public async Task StartAsync(CancellationToken cancellationToken) { - var reattachInfo = await sessionPersistor.Load(cancellationToken).ConfigureAwait(false); + var reattachInfo = await SessionPersistor.Load(cancellationToken).ConfigureAwait(false); if (!autoStart && reattachInfo == null) return; @@ -375,26 +370,8 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - public async Task StopAsync(CancellationToken cancellationToken) - { - await TerminateNoLock(false, !releaseServers, cancellationToken).ConfigureAwait(false); - if (releasedReattachInformation != null) - { - try - { - await sessionPersistor.Save(releasedReattachInformation, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.LogCritical( - ex, - "Failed to persist session reattach information! To repair this, DreamDaemon will need to be manully stopped and then relaunched with TGS."); - } - - releasedReattachInformation = null; - releaseServers = false; - } - } + public Task StopAsync(CancellationToken cancellationToken) => + TerminateNoLock(false, !releaseServers, cancellationToken); /// public async Task Terminate(bool graceful, CancellationToken cancellationToken) @@ -477,7 +454,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// to use, if any. /// The for the operation. /// A representing the running operation. - protected abstract Task InitControllers(Task chatTask, ReattachInformation reattachInfo, CancellationToken cancellationToken); + protected abstract Task InitController(Task chatTask, ReattachInformation reattachInfo, CancellationToken cancellationToken); /// /// Launches the watchdog. @@ -525,11 +502,11 @@ namespace Tgstation.Server.Host.Components.Watchdog try { - await InitControllers(announceTask, reattachInfo, cancellationToken).ConfigureAwait(false); + await InitController(announceTask, reattachInfo, cancellationToken).ConfigureAwait(false); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { - Logger.LogTrace("Controller initialization canceled!"); + Logger.LogTrace(ex, "Controller initialization cancelled!"); throw; } catch (Exception e) @@ -610,7 +587,7 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - /// Call from when a reattach operation fails to attempt a fresh start. + /// Call from when a reattach operation fails to attempt a fresh start. /// /// The for the operation. /// A representing the running operation. @@ -623,7 +600,7 @@ namespace Tgstation.Server.Host.Components.Watchdog Logger.LogWarning(FailReattachMessage); var chatTask = Chat.QueueWatchdogMessage(FailReattachMessage, cancellationToken); - await InitControllers(chatTask, null, cancellationToken).ConfigureAwait(false); + await InitController(chatTask, null, cancellationToken).ConfigureAwait(false); } /// @@ -641,7 +618,11 @@ namespace Tgstation.Server.Host.Components.Watchdog { Logger.LogTrace("DisposeAndNullControllers"); using (await SemaphoreSlimContext.Lock(controllerDisposeSemaphore, cancellationToken).ConfigureAwait(false)) + { await DisposeAndNullControllersImpl().ConfigureAwait(false); + if (!releaseServers) + await SessionPersistor.Clear(cancellationToken).ConfigureAwait(false); + } } /// @@ -931,8 +912,9 @@ namespace Tgstation.Server.Host.Components.Watchdog if (releaseServers) { - Logger.LogTrace("Detaching servers..."); - releasedReattachInformation = await GetActiveController().Release().ConfigureAwait(false); + Logger.LogTrace("Detaching server..."); + var controller = GetActiveController(); + await controller.Release().ConfigureAwait(false); } }