tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
DmbFactory.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Diagnostics.CodeAnalysis;
4using System.IO;
5using System.Linq;
6using System.Runtime.CompilerServices;
7using System.Text;
8using System.Threading;
9using System.Threading.Tasks;
10
11using Microsoft.EntityFrameworkCore;
12using Microsoft.Extensions.Logging;
13
22
24{
29 {
31 public Task OnNewerDmb
32 {
33 get
34 {
35 lock (jobLockManagers)
36 return newerDmbTcs.Task;
37 }
38 }
39
41 [MemberNotNullWhen(true, nameof(nextLockManager))]
42 public bool DmbAvailable => nextLockManager != null;
43
48
53
58
62 readonly ILogger<DmbFactory> logger;
63
68
73
78
82 readonly CancellationTokenSource cleanupCts;
83
87 readonly CancellationTokenSource lockLogCts;
88
92 readonly Dictionary<long, DeploymentLockManager> jobLockManagers;
93
97 volatile TaskCompletionSource newerDmbTcs;
98
103
108
113
130 ILogger<DmbFactory> logger,
131 Api.Models.Instance metadata)
132 {
133 this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
134 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
135 this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
136 this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
137 this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
138 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
139 this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
140
141 cleanupTask = Task.CompletedTask;
142 newerDmbTcs = new TaskCompletionSource();
143 cleanupCts = new CancellationTokenSource();
144 lockLogCts = new CancellationTokenSource();
145 jobLockManagers = new Dictionary<long, DeploymentLockManager>();
146 }
147
149 public void Dispose()
150 {
151 // we don't dispose nextDmbProvider here, since it might be the only thing we have
152 lockLogCts.Dispose();
153 cleanupCts.Dispose();
154 }
155
157 public async ValueTask LoadCompileJob(CompileJob job, Action<bool>? activationAction, CancellationToken cancellationToken)
158 {
159 ArgumentNullException.ThrowIfNull(job);
160
161 var (dmbProvider, lockManager) = await FromCompileJobInternal(job, "Compile job loading", cancellationToken);
162 if (dmbProvider == null)
163 return;
164
165 if (lockManager == null)
166 throw new InvalidOperationException($"We did not acquire the first lock for compile job {job.Id}!");
167
168 // Do this first, because it's entirely possible when we set the tcs it will immediately need to be applied
169 if (started)
170 {
172 metadata,
173 job);
174 await remoteDeploymentManager.StageDeployment(
175 lockManager.CompileJob,
176 activationAction,
177 cancellationToken);
178 }
179
180 ValueTask dmbDisposeTask;
181 lock (jobLockManagers)
182 {
183 dmbDisposeTask = nextLockManager?.DisposeAsync() ?? ValueTask.CompletedTask;
184 nextLockManager = lockManager;
185
186 // Oh god dammit
187 var temp = Interlocked.Exchange(ref newerDmbTcs, new TaskCompletionSource());
188 temp.SetResult();
189 }
190
191 await dmbDisposeTask;
192 }
193
195 public IDmbProvider LockNextDmb(string reason, [CallerFilePath] string? callerFile = null, [CallerLineNumber] int callerLine = default)
196 {
197 if (!DmbAvailable)
198 throw new InvalidOperationException("No .dmb available!");
199
200 return nextLockManager.AddLock(reason, callerFile, callerLine);
201 }
202
204 public async Task StartAsync(CancellationToken cancellationToken)
205 {
206 CompileJob? cj = null;
208 async (db) =>
209 cj = await db
210 .CompileJobs
211 .AsQueryable()
212 .Where(x => x.Job.Instance!.Id == metadata.Id)
213 .OrderByDescending(x => x.Job.StoppedAt)
214 .FirstOrDefaultAsync(cancellationToken));
215
216 try
217 {
218 if (cj == default(CompileJob))
219 return;
220 await LoadCompileJob(cj, null, cancellationToken);
221 }
222 finally
223 {
224 started = true;
225 }
226
227 // we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet
228 cleanupTask = Task.WhenAll(cleanupTask, LogLockStates());
229 }
230
232 public async Task StopAsync(CancellationToken cancellationToken)
233 {
234 try
235 {
236 lockLogCts.Cancel();
237
238 lock (jobLockManagers)
240
241 using (cancellationToken.Register(() => cleanupCts.Cancel()))
242 await cleanupTask;
243 }
244 finally
245 {
246 started = false;
247 }
248 }
249
251#pragma warning disable CA1506 // TODO: Decomplexify
252 public async ValueTask<IDmbProvider?> FromCompileJob(CompileJob compileJob, string reason, CancellationToken cancellationToken, [CallerFilePath] string? callerFile = null, [CallerLineNumber] int callerLine = default)
253 {
254 ArgumentNullException.ThrowIfNull(compileJob);
255 ArgumentNullException.ThrowIfNull(reason);
256
257 var (dmb, _) = await FromCompileJobInternal(compileJob, reason, cancellationToken, callerFile, callerLine);
258
259 return dmb;
260 }
261
263#pragma warning disable CA1506 // TODO: Decomplexify
264 public async ValueTask CleanUnusedCompileJobs(CancellationToken cancellationToken)
265 {
266 List<long> jobIdsToSkip;
267
268 // don't clean locked directories
269 lock (jobLockManagers)
270 jobIdsToSkip = jobLockManagers.Keys.ToList();
271
272 List<string>? jobUidsToNotErase = null;
273
274 // find the uids of locked directories
275 if (jobIdsToSkip.Count > 0)
276 {
277 await databaseContextFactory.UseContext(async db =>
278 {
279 jobUidsToNotErase = (await db
280 .CompileJobs
281 .AsQueryable()
282 .Where(
283 x => x.Job.Instance!.Id == metadata.Id
284 && jobIdsToSkip.Contains(x.Id!.Value))
285 .Select(x => x.DirectoryName!.Value)
286 .ToListAsync(cancellationToken))
287 .Select(x => x.ToString())
288 .ToList();
289 });
290 }
291 else
292 jobUidsToNotErase = new List<string>();
293
294 jobUidsToNotErase!.Add(SwappableDmbProvider.LiveGameDirectory);
295
296 logger.LogTrace("We will not clean the following directories: {directoriesToNotClean}", String.Join(", ", jobUidsToNotErase));
297
298 // cleanup
299 var gameDirectory = ioManager.ResolvePath();
300 await ioManager.CreateDirectory(gameDirectory, cancellationToken);
301 var directories = await ioManager.GetDirectories(gameDirectory, cancellationToken);
302 int deleting = 0;
303 var tasks = directories.Select<string, ValueTask>(async x =>
304 {
305 var nameOnly = ioManager.GetFileName(x);
306 if (jobUidsToNotErase.Contains(nameOnly))
307 return;
308 logger.LogDebug("Cleaning unused game folder: {dirName}...", nameOnly);
309 try
310 {
311 ++deleting;
312 await DeleteCompileJobContent(x, cancellationToken);
313 }
314 catch (Exception e) when (e is not OperationCanceledException)
315 {
316 logger.LogWarning(e, "Error deleting directory {dirName}!", x);
317 }
318 }).ToList();
319 if (deleting > 0)
320 await ValueTaskExtensions.WhenAll(tasks);
321 }
322#pragma warning restore CA1506
323
325 public async ValueTask<CompileJob?> LatestCompileJob()
326 {
327 if (!DmbAvailable)
328 return null;
329
330 await using IDmbProvider provider = LockNextDmb("Checking latest CompileJob");
331
332 return provider.CompileJob;
333 }
334
344 async ValueTask<(IDmbProvider? DmbProvider, DeploymentLockManager? LockManager)> FromCompileJobInternal(CompileJob compileJob, string reason, CancellationToken cancellationToken, [CallerFilePath] string? callerFile = null, [CallerLineNumber] int callerLine = default)
345 {
346 // ensure we have the entire metadata tree
347 var compileJobId = compileJob.Require(x => x.Id);
348 lock (jobLockManagers)
349 if (jobLockManagers.TryGetValue(compileJobId, out var lockManager))
350 return (DmbProvider: lockManager.AddLock(reason, callerFile, callerLine), LockManager: null); // fast path
351
352 logger.LogTrace("Loading compile job {id}...", compileJobId);
354 async db => compileJob = await db
355 .CompileJobs
356 .AsQueryable()
357 .Where(x => x!.Id == compileJobId)
358 .Include(x => x.Job!)
359 .ThenInclude(x => x.StartedBy)
360 .Include(x => x.Job!)
361 .ThenInclude(x => x.Instance)
362 .Include(x => x.RevisionInformation!)
363 .ThenInclude(x => x.PrimaryTestMerge!)
364 .ThenInclude(x => x.MergedBy)
365 .Include(x => x.RevisionInformation!)
366 .ThenInclude(x => x.ActiveTestMerges!)
367 .ThenInclude(x => x.TestMerge!)
368 .ThenInclude(x => x.MergedBy)
369 .FirstAsync(cancellationToken)); // can't wait to see that query
370
371 EngineVersion engineVersion;
372 if (!EngineVersion.TryParse(compileJob.EngineVersion, out var engineVersionNullable))
373 {
374 logger.LogError("Error loading compile job, bad engine version: {engineVersion}", compileJob.EngineVersion);
375 return (null, null); // omae wa mou shinderu
376 }
377 else
378 engineVersion = engineVersionNullable!;
379
380 if (!compileJob.Job.StoppedAt.HasValue)
381 {
382 // This happens when we're told to load the compile job that is currently finished up
383 // It constitutes an API violation if it's returned by the DreamDaemonController so just set it here
384 // Bit of a hack, but it works out to be nearly if not the same value that's put in the DB
385 logger.LogTrace("Setting missing StoppedAt for CompileJob.Job #{id}...", compileJob.Job.Id);
386 compileJob.Job.StoppedAt = DateTimeOffset.UtcNow;
387 }
388
389 var providerSubmitted = false;
390 void CleanupAction()
391 {
392 if (providerSubmitted)
393 CleanRegisteredCompileJob(compileJob);
394 }
395
396 var newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction));
397 try
398 {
399 const string LegacyADirectoryName = "A";
400 const string LegacyBDirectoryName = "B";
401
402 var dmbExistsAtRoot = await ioManager.FileExists(
404 newProvider.Directory,
405 newProvider.DmbName),
406 cancellationToken);
407
408 if (!dmbExistsAtRoot)
409 {
410 logger.LogTrace("Didn't find .dmb at game directory root, checking A/B dirs...");
411 var primaryCheckTask = ioManager.FileExists(
413 newProvider.Directory,
414 LegacyADirectoryName,
415 newProvider.DmbName),
416 cancellationToken);
417 var secondaryCheckTask = ioManager.FileExists(
419 newProvider.Directory,
420 LegacyBDirectoryName,
421 newProvider.DmbName),
422 cancellationToken);
423
424 if (!(await primaryCheckTask && await secondaryCheckTask))
425 {
426 logger.LogWarning("Error loading compile job, .dmb missing!");
427 return (null, null); // omae wa mou shinderu
428 }
429
430 // rebuild the provider because it's using the legacy style directories
431 // Don't dispose it
432 logger.LogDebug("Creating legacy two folder .dmb provider targeting {aDirName} directory...", LegacyADirectoryName);
433#pragma warning disable CA2000 // Dispose objects before losing scope (false positive)
434 newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction), Path.DirectorySeparatorChar + LegacyADirectoryName);
435#pragma warning restore CA2000 // Dispose objects before losing scope
436 }
437
438 lock (jobLockManagers)
439 {
440 IDmbProvider lockedProvider;
441 if (!jobLockManagers.TryGetValue(compileJobId, out var lockManager))
442 {
443 lockManager = DeploymentLockManager.Create(newProvider, logger, reason, out lockedProvider);
444 jobLockManagers.Add(compileJobId, lockManager);
445
446 providerSubmitted = true;
447 }
448 else
449 {
450 lockedProvider = lockManager.AddLock(reason, callerFile, callerLine); // race condition
451 lockManager = null;
452 }
453
454 return (DmbProvider: lockedProvider, LockManager: lockManager);
455 }
456 }
457 finally
458 {
459 if (!providerSubmitted)
460 await newProvider.DisposeAsync();
461 }
462 }
463
469 {
470 Task HandleCleanup()
471 {
472 lock (jobLockManagers)
473 jobLockManagers.Remove(job.Require(x => x.Id));
474
475 var otherTask = cleanupTask;
476
477 async Task WrapThrowableTasks()
478 {
479 try
480 {
481 // First kill the GitHub deployment
483
484 var cancellationToken = cleanupCts.Token;
485 var deploymentJob = remoteDeploymentManager.MarkInactive(job, cancellationToken);
486
487 var deleteTask = DeleteCompileJobContent(job.DirectoryName!.Value.ToString(), cancellationToken);
488
489 await ValueTaskExtensions.WhenAll(deleteTask, deploymentJob);
490 }
491 catch (Exception ex) when (ex is not OperationCanceledException)
492 {
493 logger.LogWarning(ex, "Error cleaning up compile job {jobGuid}!", job.DirectoryName);
494 }
495 }
496
497 return Task.WhenAll(otherTask, WrapThrowableTasks());
498 }
499
500 lock (cleanupCts)
501 cleanupTask = HandleCleanup();
502 }
503
510 async ValueTask DeleteCompileJobContent(string directory, CancellationToken cancellationToken)
511 {
512 // Then call the cleanup event, waiting here first
513 await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List<string> { ioManager.ResolvePath(directory) }, true, cancellationToken);
514 await ioManager.DeleteDirectory(directory, cancellationToken);
515 }
516
521 async Task LogLockStates()
522 {
523 logger.LogTrace("Entering lock logging loop");
524 CancellationToken cancellationToken = lockLogCts.Token;
525
526 while (!cancellationToken.IsCancellationRequested)
527 try
528 {
529 var builder = new StringBuilder();
530
531 lock (jobLockManagers)
532 foreach (var lockManager in jobLockManagers.Values)
533 lockManager.LogLockStats(builder);
534
535 logger.LogTrace("Periodic deployment log states report:{newLine}{report}", Environment.NewLine, builder);
536
537 await asyncDelayer.Delay(TimeSpan.FromMinutes(10), cancellationToken);
538 }
539 catch (OperationCanceledException ex)
540 {
541 logger.LogTrace(ex, "Exiting lock logging loop");
542 break;
543 }
544 }
545 }
546}
Information about an engine installation.
static bool TryParse(string input, out EngineVersion? engineVersion)
Attempts to parse a stringified EngineVersion.
virtual ? long Id
The ID of the entity.
Definition EntityId.cs:13
Metadata about a server instance.
Definition Instance.cs:9
Guid? DirectoryName
The Game folder the results were compiled into.
Definition CompileJob.cs:28
DateTimeOffset? StoppedAt
When the Job stopped.
Definition Job.cs:48
Extension methods for the ValueTask and ValueTask<TResult> classes.
static async ValueTask WhenAll(IEnumerable< ValueTask > tasks)
Fully await a given list of tasks .
IDmbProvider AddLock(string reason, [CallerFilePath] string? callerFile=null, [CallerLineNumber]int callerLine=default)
Add a lock to the managed IDmbProvider.
static DeploymentLockManager Create(IDmbProvider dmbProvider, ILogger logger, string initialLockReason, out IDmbProvider firstLock, [CallerFilePath] string? callerFile=null, [CallerLineNumber] int callerLine=default)
Create a DeploymentLockManager.
readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory
The IRemoteDeploymentManagerFactory for the DmbFactory.
Definition DmbFactory.cs:57
async ValueTask CleanUnusedCompileJobs(CancellationToken cancellationToken)
Deletes all compile jobs that are inactive in the Game folder.A ValueTask representing the running op...
DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, IEventConsumer eventConsumer, IAsyncDelayer asyncDelayer, ILogger< DmbFactory > logger, Api.Models.Instance metadata)
Initializes a new instance of the DmbFactory class.
readonly IIOManager ioManager
The IIOManager for the DmbFactory.
Definition DmbFactory.cs:52
IDmbProvider LockNextDmb(string reason, [CallerFilePath] string? callerFile=null, [CallerLineNumber] int callerLine=default)
Gets the next IDmbProvider. DmbAvailable is a precondition.A new IDmbProvider.
async ValueTask LoadCompileJob(CompileJob job, Action< bool >? activationAction, CancellationToken cancellationToken)
Load a new job into the ICompileJobSink.A ValueTask representing the running operation.
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the DmbFactory.
Definition DmbFactory.cs:72
readonly IEventConsumer eventConsumer
The IEventConsumer for the DmbFactory.
Definition DmbFactory.cs:67
readonly CancellationTokenSource lockLogCts
The CancellationTokenSource for LogLockStates.
Definition DmbFactory.cs:87
bool DmbAvailable
If LockNextDmb will succeed.
Definition DmbFactory.cs:42
volatile TaskCompletionSource newerDmbTcs
TaskCompletionSource resulting in the latest DmbProvider yet to exist.
Definition DmbFactory.cs:97
readonly ILogger< DmbFactory > logger
The ILogger for the DmbFactory.
Definition DmbFactory.cs:62
void CleanRegisteredCompileJob(CompileJob job)
Delete the Api.Models.Internal.CompileJob.DirectoryName of job .
async Task StopAsync(CancellationToken cancellationToken)
readonly Api.Models.Instance metadata
The Api.Models.Instance for the DmbFactory.
Definition DmbFactory.cs:77
async Task LogLockStates()
Lock all DeploymentLockManagers states.
async ValueTask<(IDmbProvider? DmbProvider, DeploymentLockManager? LockManager)> FromCompileJobInternal(CompileJob compileJob, string reason, CancellationToken cancellationToken, [CallerFilePath] string? callerFile=null, [CallerLineNumber] int callerLine=default)
Gets a IDmbProvider and potentially the DeploymentLockManager for a given CompileJob.
readonly Dictionary< long, DeploymentLockManager > jobLockManagers
Map of CompileJob.JobIds to locks on them.
Definition DmbFactory.cs:92
DeploymentLockManager? nextLockManager
The DeploymentLockManager for the latest DmbProvider.
Task OnNewerDmb
Get a Task that completes when the result of a call to LockNextDmb will be different than the previou...
Definition DmbFactory.cs:32
async ValueTask< CompileJob?> LatestCompileJob()
Gets the latest CompileJob.A ValueTask<TResult> resulting in the latest CompileJob or null if none ar...
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the DmbFactory.
Definition DmbFactory.cs:47
readonly CancellationTokenSource cleanupCts
The CancellationTokenSource for cleanupTask.
Definition DmbFactory.cs:82
bool started
If the DmbFactory is "started" via IComponentService.
async ValueTask DeleteCompileJobContent(string directory, CancellationToken cancellationToken)
Handles cleaning the resources of a CompileJob.
async Task StartAsync(CancellationToken cancellationToken)
async ValueTask< IDmbProvider?> FromCompileJob(CompileJob compileJob, string reason, CancellationToken cancellationToken, [CallerFilePath] string? callerFile=null, [CallerLineNumber] int callerLine=default)
Gets a IDmbProvider for a given CompileJob.A ValueTask<TResult> resulting in a new IDmbProvider repre...
Task cleanupTask
Task representing calls to CleanRegisteredCompileJob(CompileJob).
A IDmbProvider that uses filesystem links to change directory structure underneath the server process...
const string LiveGameDirectory
The directory where the BaseProvider is symlinked to.
Job Job
See CompileJobResponse.Job.
Definition CompileJob.cs:16
string EngineVersion
The Version the CompileJob was made with in string form.
Definition CompileJob.cs:33
Runs a given disposeAction on Dispose.
Provides absolute paths to the latest compiled .dmbs.
IRemoteDeploymentManager CreateRemoteDeploymentManager(Api.Models.Instance metadata, RemoteGitProvider remoteGitProvider)
Creates a IRemoteDeploymentManager for a given remoteGitProvider .
void ForgetLocalStateForCompileJobs(IEnumerable< long > compileJobsIds)
Cause the IRemoteDeploymentManagerFactory to drop any local state is has for the given compileJobsIds...
ValueTask MarkInactive(CompileJob compileJob, CancellationToken cancellationToken)
Mark the deplotment for a given compileJob as inactive.
ValueTask StageDeployment(CompileJob compileJob, Action< bool >? activationCallback, CancellationToken cancellationToken)
Stage a given compileJob 's deployment.
Consumes EventTypes and takes the appropriate actions.
ValueTask HandleEvent(EventType eventType, IEnumerable< string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
Handle a given eventType .
Factory for scoping usage of IDatabaseContexts. Meant for use by Components.
ValueTask UseContext(Func< IDatabaseContext, ValueTask > operation)
Run an operation in the scope of an IDatabaseContext.
Interface for using filesystems.
Definition IIOManager.cs:13
string GetFileName(string path)
Gets the file name portion of a path .
string ResolvePath()
Retrieve the full path of the current working directory.
string ConcatPath(params string[] paths)
Combines an array of strings into a path.
Task< IReadOnlyList< string > > GetDirectories(string path, CancellationToken cancellationToken)
Returns full directory names in a given path .
Task CreateDirectory(string path, CancellationToken cancellationToken)
Create a directory at path .
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory, removes and does not enter any symlinks encounterd.
Task< bool > FileExists(string path, CancellationToken cancellationToken)
Check that the file at path exists.
ValueTask Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
Create a Task that completes after a given timeSpan .
EventType
Types of events. Mirror in tgs.dm. Prefer last listed name for script.
Definition EventType.cs:7