tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
AdvancedWatchdog.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Diagnostics;
4using System.Threading;
5using System.Threading.Tasks;
6
7using Microsoft.Extensions.Logging;
8
20
22{
27 {
31 protected SwappableDmbProvider? ActiveSwappable { get; private set; }
32
37
41 readonly List<Task> deploymentCleanupTasks;
42
47
51 volatile TaskCompletionSource? deploymentCleanupGate;
52
73 IChatManager chat,
74 ISessionControllerFactory sessionControllerFactory,
75 IDmbFactory dmbFactory,
76 ISessionPersistor sessionPersistor,
78 IServerControl serverControl,
79 IAsyncDelayer asyncDelayer,
83 IIOManager gameIOManager,
84 IFilesystemLinkFactory linkFactory,
85 ILogger<AdvancedWatchdog> logger,
86 DreamDaemonLaunchParameters initialLaunchParameters,
87 Api.Models.Instance instance,
88 bool autoStart)
89 : base(
90 chat,
91 sessionControllerFactory,
92 dmbFactory,
93 sessionPersistor,
95 serverControl,
96 asyncDelayer,
100 gameIOManager,
101 logger,
102 initialLaunchParameters,
103 instance,
104 autoStart)
105 {
106 try
107 {
108 LinkFactory = linkFactory ?? throw new ArgumentNullException(nameof(linkFactory));
109
110 deploymentCleanupTasks = new List<Task>();
111 }
112 catch
113 {
114 // Async dispose is for if we have controllers running, not the case here
115 var disposeTask = DisposeAsync();
116 Debug.Assert(disposeTask.IsCompleted, "This should always be true during construction!");
117 disposeTask.GetAwaiter().GetResult();
118
119 throw;
120 }
121 }
122
124 protected sealed override async ValueTask DisposeAndNullControllersImpl()
125 {
126 await base.DisposeAndNullControllersImpl();
127
128 // If we reach this point, we can guarantee PrepServerForLaunch will be called before starting again.
129 ActiveSwappable = null;
130
131 if (pendingSwappable != null)
132 {
134 pendingSwappable = null;
135 }
136
137 await DrainDeploymentCleanupTasks(true);
138 }
139
141 protected sealed override async ValueTask<MonitorAction> HandleNormalReboot(CancellationToken cancellationToken)
142 {
143 if (pendingSwappable != null)
144 {
145 var needToSwap = !pendingSwappable.Swapped;
146 var controller = Server!;
147 if (needToSwap)
148 {
149 // IMPORTANT: THE SESSIONCONTROLLER SHOULD STILL BE PROCESSING THE BRIDGE REQUEST SO WE KNOW DD IS SLEEPING
150 // OTHERWISE, IT COULD RETURN TO /world/Reboot() TOO EARLY AND LOAD THE WRONG .DMB
151 if (!controller.ProcessingRebootBridgeRequest)
152 {
153 // integration test logging will catch this
154 Logger.LogError(
155 "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");
156 return MonitorAction.Restart;
157 }
158
159 // DCT: Not necessary
160 if (!pendingSwappable.FinishActivationPreparation(CancellationToken.None).IsCompleted)
161 {
162 // rare pokemon
163 Logger.LogInformation("Deployed .dme is not ready to swap, delaying until next reboot!");
164 Chat.QueueWatchdogMessage("The pending deployment was not ready to be activated this reboot. It will be applied at the next one.");
165 return MonitorAction.Continue;
166 }
167 }
168
169 var updateTask = BeforeApplyDmb(pendingSwappable.CompileJob, cancellationToken);
170 if (needToSwap)
171 await PerformDmbSwap(pendingSwappable, cancellationToken);
172
173 var currentCompileJobId = controller.ReattachInformation.Dmb.CompileJob.Id;
174
175 await DrainDeploymentCleanupTasks(false);
176
177 IAsyncDisposable lingeringDeployment;
178 var localDeploymentCleanupGate = new TaskCompletionSource();
179 async Task CleanupLingeringDeployment()
180 {
181 var lingeringDeploymentExpirySeconds = ActiveLaunchParameters.StartupTimeout!.Value;
182 Logger.LogDebug(
183 "Holding old deployment {compileJobId} for up to {expiry} seconds...",
184 currentCompileJobId,
185 lingeringDeploymentExpirySeconds);
186
187 // DCT: A cancel firing here can result in us leaving a dmbprovider undisposed, localDeploymentCleanupGate will always fire in that case
188 var timeout = AsyncDelayer.Delay(TimeSpan.FromSeconds(lingeringDeploymentExpirySeconds), CancellationToken.None).AsTask();
189
190 var completedTask = await Task.WhenAny(
191 localDeploymentCleanupGate.Task,
192 timeout);
193
194 var timedOut = completedTask == timeout;
195 Logger.Log(
196 timedOut
197 ? LogLevel.Warning
198 : LogLevel.Trace,
199 "Releasing old deployment {compileJobId}{afterTimeout}",
200 currentCompileJobId,
201 timedOut
202 ? " due to timeout!"
203 : "...");
204
205 await lingeringDeployment.DisposeAsync();
206 }
207
208 var oldDeploymentCleanupGate = Interlocked.Exchange(ref deploymentCleanupGate, localDeploymentCleanupGate);
209 oldDeploymentCleanupGate?.TrySetResult();
210
211 Logger.LogTrace("Replacing activeSwappable with pendingSwappable...");
212
214 {
215 lingeringDeployment = controller.ReplaceDmbProvider(pendingSwappable);
217 CleanupLingeringDeployment());
218 }
219
221 pendingSwappable = null;
222
223 await SessionPersistor.Update(controller.ReattachInformation, cancellationToken);
224 await updateTask;
225 }
226 else
227 Logger.LogTrace("Nothing to do as pendingSwappable is null.");
228
229 return await base.HandleNormalReboot(cancellationToken);
230 }
231
233 protected sealed override async ValueTask HandleNewDmbAvailable(CancellationToken cancellationToken)
234 {
235 IDmbProvider compileJobProvider = DmbFactory.LockNextDmb("AdvancedWatchdog next compile job preload");
236 bool canSeamlesslySwap = CanUseSwappableDmbProvider(compileJobProvider);
237 if (canSeamlesslySwap)
238 if (compileJobProvider.CompileJob.EngineVersion != ActiveCompileJob!.EngineVersion)
239 {
240 // have to do a graceful restart
241 Logger.LogDebug(
242 "Not swapping to new compile job {compileJobId} as it uses a different engine version ({newEngineVersion}) than what is currently active {oldEngineVersion}.",
243 compileJobProvider.CompileJob.Id,
244 compileJobProvider.CompileJob.EngineVersion,
245 ActiveCompileJob.EngineVersion);
246 canSeamlesslySwap = false;
247 }
248 else if (compileJobProvider.CompileJob.DmeName != ActiveCompileJob.DmeName)
249 {
250 Logger.LogDebug(
251 "Not swapping to new compile job {compileJobId} as it uses a different .dmb name ({newDmbName}) than what is currently active {oldDmbName}.",
252 compileJobProvider.CompileJob.Id,
253 compileJobProvider.CompileJob.DmeName,
255 canSeamlesslySwap = false;
256 }
257
258 if (!canSeamlesslySwap)
259 {
260 Logger.LogDebug("Queueing graceful restart instead...");
261 await compileJobProvider.DisposeAsync();
262 await base.HandleNewDmbAvailable(cancellationToken);
263 return;
264 }
265
266 SwappableDmbProvider? swappableProvider = null;
267 try
268 {
269 swappableProvider = CreateSwappableDmbProvider(compileJobProvider);
270 if (ActiveCompileJob!.DMApiVersion == null)
271 {
272 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");
273 await PerformDmbSwap(swappableProvider, cancellationToken);
274 }
275 }
276 catch (Exception ex)
277 {
278 Logger.LogError(ex, "Exception while swapping");
279 IDmbProvider providerToDispose = swappableProvider ?? compileJobProvider;
280 await providerToDispose.DisposeAsync();
281 throw;
282 }
283
284 await (pendingSwappable?.DisposeAsync() ?? ValueTask.CompletedTask);
285 pendingSwappable = swappableProvider;
286 }
287
289 protected sealed override async ValueTask<IDmbProvider> PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken)
290 {
291 if (ActiveSwappable != null)
292 throw new InvalidOperationException("Expected activeSwappable to be null!");
293 if (pendingSwappable != null)
294 throw new InvalidOperationException("Expected pendingSwappable to be null!");
295
296 Logger.LogTrace("Prep for server launch");
297 if (!CanUseSwappableDmbProvider(dmbToUse))
298 return dmbToUse;
299
301 try
302 {
303 await InitialLink(cancellationToken);
304 }
305 catch (Exception ex)
306 {
307 // We won't worry about disposing activeSwappable here as we can't dispose dmbToUse here.
308 Logger.LogTrace(ex, "Initial link error, nulling ActiveSwappable");
309 ActiveSwappable = null;
310 throw;
311 }
312
313 return ActiveSwappable;
314 }
315
321 protected abstract ValueTask ApplyInitialDmb(CancellationToken cancellationToken);
322
329
331 protected override async ValueTask SessionStartupPersist(CancellationToken cancellationToken)
332 {
333 await ApplyInitialDmb(cancellationToken);
334 await base.SessionStartupPersist(cancellationToken);
335 }
336
338 protected override async ValueTask<MonitorAction> HandleMonitorWakeup(MonitorActivationReason reason, CancellationToken cancellationToken)
339 {
340 var result = await base.HandleMonitorWakeup(reason, cancellationToken);
341 if (reason == MonitorActivationReason.ActiveServerStartup)
342 await DrainDeploymentCleanupTasks(false);
343
344 return result;
345 }
346
353 {
354 if (dmbProvider.EngineVersion.Engine != EngineType.Byond)
355 {
356 Logger.LogDebug("Not using SwappableDmbProvider for engine type {engineType}", dmbProvider.EngineVersion.Engine);
357 return false;
358 }
359
360 return true;
361 }
362
368 async ValueTask InitialLink(CancellationToken cancellationToken)
369 {
370 await ActiveSwappable!.FinishActivationPreparation(cancellationToken);
371 Logger.LogTrace("Linking compile job...");
372 await ActiveSwappable.MakeActive(cancellationToken);
373 }
374
381 async ValueTask PerformDmbSwap(SwappableDmbProvider newProvider, CancellationToken cancellationToken)
382 {
383 Logger.LogDebug("Swapping to compile job {id}...", newProvider.CompileJob.Id);
384
385 await newProvider.FinishActivationPreparation(cancellationToken);
386
387 var suspended = false;
388 var server = Server!;
389 try
390 {
391 server.SuspendProcess();
392 suspended = true;
393 }
394 catch (Exception ex)
395 {
396 Logger.LogWarning(ex, "Exception while suspending server!");
397 }
398
399 try
400 {
401 Logger.LogTrace("Making new provider {id} active...", newProvider.CompileJob.Id);
402 await newProvider.MakeActive(cancellationToken);
403 }
404 finally
405 {
406 // Let this throw hard if it fails
407 if (suspended)
408 server.ResumeProcess();
409 }
410 }
411
417 Task DrainDeploymentCleanupTasks(bool blocking)
418 {
419 Logger.LogTrace("DrainDeploymentCleanupTasks...");
420 var localDeploymentCleanupGate = Interlocked.Exchange(ref deploymentCleanupGate, null);
421 localDeploymentCleanupGate?.TrySetResult();
422
423 List<Task> localDeploymentCleanupTasks;
425 {
426 var totalActiveTasks = deploymentCleanupTasks.Count;
427 localDeploymentCleanupTasks = new List<Task>(totalActiveTasks);
428 for (var i = totalActiveTasks - 1; i >= 0; --i)
429 {
430 var currentTask = deploymentCleanupTasks[i];
431 if (!blocking && !currentTask.IsCompleted)
432 continue;
433
434 localDeploymentCleanupTasks.Add(currentTask);
435 deploymentCleanupTasks.RemoveAt(i);
436 }
437 }
438
439 return Task.WhenAll(localDeploymentCleanupTasks);
440 }
441 }
442}
virtual ? long Id
The ID of the entity.
Definition EntityId.cs:13
string? DmeName
The .dme file used for compilation.
Definition CompileJob.cs:16
IDmbProvider LockNextDmb(string reason, [CallerFilePath] string? callerFile=null, [CallerLineNumber] int callerLine=default)
Gets the next IDmbProvider. DmbAvailable is a precondition.A new IDmbProvider.
A IDmbProvider that uses filesystem links to change directory structure underneath the server process...
Task FinishActivationPreparation(CancellationToken cancellationToken)
Should be awaited. before calling MakeActive(CancellationToken) to ensure the SwappableDmbProvider is...
bool Swapped
If MakeActive(CancellationToken) has been run.
ValueTask MakeActive(CancellationToken cancellationToken)
Make the SwappableDmbProvider active by replacing the live link with our CompileJob.
ValueTask Update(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Update some reattachInformation .A ValueTask representing the running operation.
A IWatchdog that, instead of killing servers for updates, uses the wonders of filesystem links to swa...
readonly List< Task > deploymentCleanupTasks
List<T> of Tasks that are waiting to clean up old deployments.
override async ValueTask< IDmbProvider > PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken)
Prepare the server to launch a new instance with the WatchdogBase.ActiveLaunchParameters and a given ...
bool CanUseSwappableDmbProvider(IDmbProvider dmbProvider)
If the SwappableDmbProvider feature of the AdvancedWatchdog can be used with a given dmbProvider .
SwappableDmbProvider? pendingSwappable
The active SwappableDmbProvider for WatchdogBase.ActiveLaunchParameters.
async ValueTask InitialLink(CancellationToken cancellationToken)
Create the initial link to the live game directory using ActiveSwappable.
SwappableDmbProvider? ActiveSwappable
The SwappableDmbProvider for WatchdogBase.LastLaunchParameters.
override async ValueTask< MonitorAction > HandleNormalReboot(CancellationToken cancellationToken)
Handler for MonitorActivationReason.ActiveServerRebooted when the RebootState is RebootState....
Task DrainDeploymentCleanupTasks(bool blocking)
Asynchronously drain deploymentCleanupTasks.
SwappableDmbProvider CreateSwappableDmbProvider(IDmbProvider dmbProvider)
Create a SwappableDmbProvider for a given dmbProvider .
override async ValueTask SessionStartupPersist(CancellationToken cancellationToken)
Called to save the current Server into the WatchdogBase.SessionPersistor when initially launched....
override async ValueTask HandleNewDmbAvailable(CancellationToken cancellationToken)
Handler for MonitorActivationReason.NewDmbAvailable.A ValueTask representing the running operation.
IFilesystemLinkFactory LinkFactory
The IFilesystemLinkFactory for the AdvancedWatchdog.
volatile? TaskCompletionSource deploymentCleanupGate
The TaskCompletionSource representing the cleanup of an unused IDmbProvider.
AdvancedWatchdog(IChatManager chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, ISessionPersistor sessionPersistor, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, IIOManager gameIOManager, IFilesystemLinkFactory linkFactory, ILogger< AdvancedWatchdog > logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Initializes a new instance of the AdvancedWatchdog class.
async ValueTask PerformDmbSwap(SwappableDmbProvider newProvider, CancellationToken cancellationToken)
Suspends the BasicWatchdog.Server and calls SwappableDmbProvider.MakeActive(CancellationToken) on a n...
ValueTask ApplyInitialDmb(CancellationToken cancellationToken)
Set the ReattachInformation.InitialDmb for the BasicWatchdog.Server.
override async ValueTask< MonitorAction > HandleMonitorWakeup(MonitorActivationReason reason, CancellationToken cancellationToken)
readonly IJobManager jobManager
The IJobManager for the WatchdogBase.
ILogger< WatchdogBase > Logger
The ILogger for the WatchdogBase.
Models.? CompileJob ActiveCompileJob
Retrieves the Models.CompileJob currently running on the server.
readonly bool autoStart
If the WatchdogBase should LaunchNoLock(bool, bool, bool, ReattachInformation, CancellationToken) in ...
readonly IEventConsumer eventConsumer
The IEventConsumer that is not the WatchdogBase.
readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory
The IRemoteDeploymentManagerFactory for the WatchdogBase.
readonly IIOManager diagnosticsIOManager
The IIOManager pointing to the Diagnostics directory.
DreamDaemonLaunchParameters ActiveLaunchParameters
The DreamDaemonLaunchParameters to be applied.
async ValueTask BeforeApplyDmb(Models.CompileJob newCompileJob, CancellationToken cancellationToken)
To be called before a given newCompileJob goes live.
IChatManager Chat
The IChatManager for the WatchdogBase.
ValueTask Restart()
Restarts the Host.A ValueTask representing the running operation.
async ValueTask Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
Create a Task that completes after a given timeSpan .A ValueTask representing the running operation.
For managing connected chat services.
void QueueWatchdogMessage(string message)
Queue a chat message to configured watchdog channels.
Provides absolute paths to the latest compiled .dmbs.
EngineVersion EngineVersion
The Api.Models.EngineVersion used to build the .dmb.
Models.CompileJob CompileJob
The CompileJob of the .dmb.
Consumes EventTypes and takes the appropriate actions.
Handles saving and loading ReattachInformation.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
Interface for using filesystems.
Definition IIOManager.cs:13
Manages the runtime of Jobs.
EngineType
The type of engine the codebase is using.
Definition EngineType.cs:7
MonitorAction
The action for the monitor loop to take when control is returned to it.
MonitorActivationReason
Reasons for the monitor to wake up.