tgstation-server 6.19.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.Linq;
5using System.Runtime.CompilerServices;
6using System.Text;
7using System.Threading;
8using System.Threading.Tasks;
9
10using Microsoft.EntityFrameworkCore;
11using Microsoft.Extensions.Logging;
12
21
23{
28 {
30 public Task OnNewerDmb
31 {
32 get
33 {
34 lock (jobLockManagers)
35 return newerDmbTcs.Task;
36 }
37 }
38
40 [MemberNotNullWhen(true, nameof(nextLockManager))]
41 public bool DmbAvailable => nextLockManager != null;
42
47
52
57
61 readonly ILogger<DmbFactory> logger;
62
67
72
77
81 readonly CancellationTokenSource cleanupCts;
82
86 readonly CancellationTokenSource lockLogCts;
87
91 readonly Dictionary<long, DeploymentLockManager> jobLockManagers;
92
96 volatile TaskCompletionSource newerDmbTcs;
97
102
107
112
129 ILogger<DmbFactory> logger,
130 Api.Models.Instance metadata)
131 {
132 this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
133 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
134 this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
135 this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
136 this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
137 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
138 this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
139
140 cleanupTask = Task.CompletedTask;
141 newerDmbTcs = new TaskCompletionSource();
142 cleanupCts = new CancellationTokenSource();
143 lockLogCts = new CancellationTokenSource();
144 jobLockManagers = new Dictionary<long, DeploymentLockManager>();
145 }
146
148 public void Dispose()
149 {
150 // we don't dispose nextDmbProvider here, since it might be the only thing we have
151 lockLogCts.Dispose();
152 cleanupCts.Dispose();
153 }
154
156 public async ValueTask LoadCompileJob(CompileJob job, Action<bool>? activationAction, CancellationToken cancellationToken)
157 {
158 ArgumentNullException.ThrowIfNull(job);
159
160 var (dmbProvider, lockManager) = await FromCompileJobInternal(job, "Compile job loading", cancellationToken);
161 if (dmbProvider == null)
162 return;
163
164 if (lockManager == null)
165 throw new InvalidOperationException($"We did not acquire the first lock for compile job {job.Id}!");
166
167 // Do this first, because it's entirely possible when we set the tcs it will immediately need to be applied
168 if (started)
169 {
171 metadata,
172 job);
173 await remoteDeploymentManager.StageDeployment(
174 lockManager.CompileJob,
175 activationAction,
176 cancellationToken);
177 }
178
179 ValueTask dmbDisposeTask;
180 lock (jobLockManagers)
181 {
182 dmbDisposeTask = nextLockManager?.DisposeAsync() ?? ValueTask.CompletedTask;
183 nextLockManager = lockManager;
184
185 // Oh god dammit
186 var temp = Interlocked.Exchange(ref newerDmbTcs, new TaskCompletionSource());
187 temp.SetResult();
188 }
189
190 await dmbDisposeTask;
191 }
192
194 public IDmbProvider LockNextDmb(string reason, [CallerFilePath] string? callerFile = null, [CallerLineNumber] int callerLine = default)
195 {
196 if (!DmbAvailable)
197 throw new InvalidOperationException("No .dmb available!");
198
199 return nextLockManager.AddLock(reason, callerFile, callerLine);
200 }
201
203 public async Task StartAsync(CancellationToken cancellationToken)
204 {
205 CompileJob? cj = null;
207 async (db) =>
208 cj = await db
209 .CompileJobs
210 .AsQueryable()
211 .Where(x => x.Job.Instance!.Id == metadata.Id)
212 .OrderByDescending(x => x.Job.StoppedAt)
213 .FirstOrDefaultAsync(cancellationToken));
214
215 try
216 {
217 if (cj == default(CompileJob))
218 return;
219 await LoadCompileJob(cj, null, cancellationToken);
220 }
221 finally
222 {
223 started = true;
224 }
225
226 // we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet
227 cleanupTask = Task.WhenAll(cleanupTask, LogLockStatesLoop());
228 }
229
231 public async Task StopAsync(CancellationToken cancellationToken)
232 {
233 try
234 {
235 lockLogCts.Cancel();
236
237 lock (jobLockManagers)
239
240 using (cancellationToken.Register(() => cleanupCts.Cancel()))
241 await cleanupTask;
242 }
243 finally
244 {
245 started = false;
246 }
247 }
248
250#pragma warning disable CA1506 // TODO: Decomplexify
251 public async ValueTask<IDmbProvider?> FromCompileJob(CompileJob compileJob, string reason, CancellationToken cancellationToken, [CallerFilePath] string? callerFile = null, [CallerLineNumber] int callerLine = default)
252 {
253 ArgumentNullException.ThrowIfNull(compileJob);
254 ArgumentNullException.ThrowIfNull(reason);
255
256 var (dmb, _) = await FromCompileJobInternal(compileJob, reason, cancellationToken, callerFile, callerLine);
257
258 return dmb;
259 }
260
262#pragma warning disable CA1506 // TODO: Decomplexify
263 public async ValueTask CleanUnusedCompileJobs(CancellationToken cancellationToken)
264 {
265 List<long> jobIdsToSkip;
266
267 // don't clean locked directories
268 lock (jobLockManagers)
269 jobIdsToSkip = jobLockManagers.Keys.ToList();
270
271 List<string>? jobUidsToNotErase = null;
272
273 // find the uids of locked directories
274 if (jobIdsToSkip.Count > 0)
275 {
276 await databaseContextFactory.UseContext(async db =>
277 {
278 jobUidsToNotErase = (await db
279 .CompileJobs
280 .AsQueryable()
281 .Where(
282 x => x.Job.Instance!.Id == metadata.Id
283 && jobIdsToSkip.Contains(x.Id!.Value))
284 .Select(x => x.DirectoryName!.Value)
285 .ToListAsync(cancellationToken))
286 .Select(x => x.ToString())
287 .ToList();
288 });
289 }
290 else
291 jobUidsToNotErase = new List<string>();
292
293 jobUidsToNotErase!.Add(SwappableDmbProvider.LiveGameDirectory);
294
295 logger.LogTrace("We will not clean the following directories: {directoriesToNotClean}", String.Join(", ", jobUidsToNotErase));
296
297 // cleanup
298 var gameDirectory = ioManager.ResolvePath();
299 await ioManager.CreateDirectory(gameDirectory, cancellationToken);
300 var directories = await ioManager.GetDirectories(gameDirectory, cancellationToken);
301 int deleting = 0;
302 var tasks = directories.Select<string, ValueTask>(async x =>
303 {
304 var nameOnly = ioManager.GetFileName(x);
305 if (jobUidsToNotErase.Contains(nameOnly))
306 return;
307 logger.LogDebug("Cleaning unused game folder: {dirName}...", nameOnly);
308 try
309 {
310 ++deleting;
311 await DeleteCompileJobContent(x, cancellationToken);
312 }
313 catch (Exception e) when (e is not OperationCanceledException)
314 {
315 logger.LogWarning(e, "Error deleting directory {dirName}!", x);
316 }
317 }).ToList();
318 if (deleting > 0)
319 await ValueTaskExtensions.WhenAll(tasks);
320 }
321#pragma warning restore CA1506
322
324 public async ValueTask<CompileJob?> LatestCompileJob()
325 {
326 if (!DmbAvailable)
327 return null;
328
329 await using IDmbProvider provider = LockNextDmb("Checking latest CompileJob");
330
331 return provider.CompileJob;
332 }
333
335 public void LogLockStates()
336 {
337 var builder = new StringBuilder();
338
339 lock (jobLockManagers)
340 foreach (var lockManager in jobLockManagers.Values)
341 lockManager.LogLockStats(builder);
342
343 logger.LogTrace("Periodic deployment log states report:{newLine}{report}", Environment.NewLine, builder);
344 }
345
355 async ValueTask<(IDmbProvider? DmbProvider, DeploymentLockManager? LockManager)> FromCompileJobInternal(CompileJob compileJob, string reason, CancellationToken cancellationToken, [CallerFilePath] string? callerFile = null, [CallerLineNumber] int callerLine = default)
356 {
357 // ensure we have the entire metadata tree
358 var compileJobId = compileJob.Require(x => x.Id);
359 lock (jobLockManagers)
360 if (jobLockManagers.TryGetValue(compileJobId, out var lockManager))
361 return (DmbProvider: lockManager.AddLock(reason, callerFile, callerLine), LockManager: null); // fast path
362
363 logger.LogTrace("Loading compile job {id}...", compileJobId);
365 async db => compileJob = await db
366 .CompileJobs
367 .AsQueryable()
368 .Where(x => x!.Id == compileJobId)
369 .Include(x => x.Job!)
370 .ThenInclude(x => x.StartedBy)
371 .Include(x => x.Job!)
372 .ThenInclude(x => x.Instance)
373 .Include(x => x.RevisionInformation!)
374 .ThenInclude(x => x.PrimaryTestMerge!)
375 .ThenInclude(x => x.MergedBy)
376 .Include(x => x.RevisionInformation!)
377 .ThenInclude(x => x.ActiveTestMerges!)
378 .ThenInclude(x => x.TestMerge!)
379 .ThenInclude(x => x.MergedBy)
380 .FirstAsync(cancellationToken)); // can't wait to see that query
381
382 EngineVersion engineVersion;
383 if (!EngineVersion.TryParse(compileJob.EngineVersion, out var engineVersionNullable))
384 {
385 logger.LogError("Error loading compile job, bad engine version: {engineVersion}", compileJob.EngineVersion);
386 return (null, null); // omae wa mou shinderu
387 }
388 else
389 engineVersion = engineVersionNullable!;
390
391 if (!compileJob.Job.StoppedAt.HasValue)
392 {
393 // This happens when we're told to load the compile job that is currently finished up
394 // It constitutes an API violation if it's returned by the DreamDaemonController so just set it here
395 // Bit of a hack, but it works out to be nearly if not the same value that's put in the DB
396 logger.LogTrace("Setting missing StoppedAt for CompileJob.Job #{id}...", compileJob.Job.Id);
397 compileJob.Job.StoppedAt = DateTimeOffset.UtcNow;
398 }
399
400 var providerSubmitted = false;
401 void CleanupAction()
402 {
403 if (providerSubmitted)
404 CleanRegisteredCompileJob(compileJob);
405 }
406
407 var newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction));
408 try
409 {
410 const string LegacyADirectoryName = "A";
411 const string LegacyBDirectoryName = "B";
412
413 var dmbExistsAtRoot = await ioManager.FileExists(
415 newProvider.Directory,
416 newProvider.DmbName),
417 cancellationToken);
418
419 if (!dmbExistsAtRoot)
420 {
421 logger.LogTrace("Didn't find .dmb at game directory root, checking A/B dirs...");
422 var primaryCheckTask = ioManager.FileExists(
424 newProvider.Directory,
425 LegacyADirectoryName,
426 newProvider.DmbName),
427 cancellationToken);
428 var secondaryCheckTask = ioManager.FileExists(
430 newProvider.Directory,
431 LegacyBDirectoryName,
432 newProvider.DmbName),
433 cancellationToken);
434
435 if (!(await primaryCheckTask && await secondaryCheckTask))
436 {
437 logger.LogWarning("Error loading compile job, .dmb missing!");
438 return (null, null); // omae wa mou shinderu
439 }
440
441 // rebuild the provider because it's using the legacy style directories
442 // Don't dispose it
443 logger.LogDebug("Creating legacy two folder .dmb provider targeting {aDirName} directory...", LegacyADirectoryName);
444#pragma warning disable CA2000 // Dispose objects before losing scope (false positive)
445 newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction), LegacyADirectoryName);
446#pragma warning restore CA2000 // Dispose objects before losing scope
447 }
448
449 lock (jobLockManagers)
450 {
451 IDmbProvider lockedProvider;
452 if (!jobLockManagers.TryGetValue(compileJobId, out var lockManager))
453 {
454 lockManager = DeploymentLockManager.Create(newProvider, logger, reason, out lockedProvider);
455 jobLockManagers.Add(compileJobId, lockManager);
456
457 providerSubmitted = true;
458 }
459 else
460 {
461 lockedProvider = lockManager.AddLock(reason, callerFile, callerLine); // race condition
462 lockManager = null;
463 }
464
465 return (DmbProvider: lockedProvider, LockManager: lockManager);
466 }
467 }
468 finally
469 {
470 if (!providerSubmitted)
471 await newProvider.DisposeAsync();
472 }
473 }
474
480 {
481 Task HandleCleanup()
482 {
483 lock (jobLockManagers)
484 jobLockManagers.Remove(job.Require(x => x.Id));
485
486 var otherTask = cleanupTask;
487
488 async Task WrapThrowableTasks()
489 {
490 try
491 {
492 // First kill the GitHub deployment
494
495 var cancellationToken = cleanupCts.Token;
496 var deploymentJob = remoteDeploymentManager.MarkInactive(job, cancellationToken);
497
498 var deleteTask = DeleteCompileJobContent(job.DirectoryName!.Value.ToString(), cancellationToken);
499
500 await ValueTaskExtensions.WhenAll(deleteTask, deploymentJob);
501 }
502 catch (Exception ex) when (ex is not OperationCanceledException)
503 {
504 logger.LogWarning(ex, "Error cleaning up compile job {jobGuid}!", job.DirectoryName);
505 }
506 }
507
508 return Task.WhenAll(otherTask, WrapThrowableTasks());
509 }
510
511 lock (cleanupCts)
512 cleanupTask = HandleCleanup();
513 }
514
521 async ValueTask DeleteCompileJobContent(string directory, CancellationToken cancellationToken)
522 {
523 // Then call the cleanup event, waiting here first
524 await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List<string> { ioManager.ResolvePath(directory) }, true, cancellationToken);
525 await ioManager.DeleteDirectory(directory, cancellationToken);
526 }
527
532 async Task LogLockStatesLoop()
533 {
534 logger.LogTrace("Entering lock logging loop");
535 CancellationToken cancellationToken = lockLogCts.Token;
536
537 while (!cancellationToken.IsCancellationRequested)
538 try
539 {
541 await asyncDelayer.Delay(TimeSpan.FromMinutes(10), cancellationToken);
542 }
543 catch (OperationCanceledException ex)
544 {
545 logger.LogTrace(ex, "Exiting lock logging loop");
546 break;
547 }
548 }
549 }
550}
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:14
Metadata about a server instance.
Definition Instance.cs:9
Guid? DirectoryName
The Game folder the results were compiled into.
Definition CompileJob.cs:29
DateTimeOffset? StoppedAt
When the Job stopped.
Definition Job.cs:49
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:56
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:51
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:71
readonly IEventConsumer eventConsumer
The IEventConsumer for the DmbFactory.
Definition DmbFactory.cs:66
readonly CancellationTokenSource lockLogCts
The CancellationTokenSource for LogLockStatesLoop.
Definition DmbFactory.cs:86
bool DmbAvailable
If LockNextDmb will succeed.
Definition DmbFactory.cs:41
volatile TaskCompletionSource newerDmbTcs
TaskCompletionSource resulting in the latest DmbProvider yet to exist.
Definition DmbFactory.cs:96
readonly ILogger< DmbFactory > logger
The ILogger for the DmbFactory.
Definition DmbFactory.cs:61
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:76
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:91
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:31
async ValueTask< CompileJob?> LatestCompileJob()
Gets the latest CompileJob.A ValueTask<TResult> resulting in the latest CompileJob or null if none ar...
void LogLockStates()
Log the states of all active IDmbProviders.
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the DmbFactory.
Definition DmbFactory.cs:46
readonly CancellationTokenSource cleanupCts
The CancellationTokenSource for cleanupTask.
Definition DmbFactory.cs:81
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...
async Task LogLockStatesLoop()
Lock all DeploymentLockManagers states.
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:14
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