tgstation-server 6.1.2
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.Threading;
7using System.Threading.Tasks;
8
9using Microsoft.EntityFrameworkCore;
10using Microsoft.Extensions.Logging;
11
20
22{
27 {
29 public Task OnNewerDmb
30 {
31 get
32 {
33 lock (jobLockCounts)
34 return newerDmbTcs.Task;
35 }
36 }
37
39 [MemberNotNullWhen(true, nameof(nextDmbProvider))]
40 public bool DmbAvailable => nextDmbProvider != null;
41
46
51
56
60 readonly ILogger<DmbFactory> logger;
61
66
71
75 readonly CancellationTokenSource cleanupCts;
76
80 readonly Dictionary<long, int> jobLockCounts;
81
85 volatile TaskCompletionSource newerDmbTcs;
86
91
96
101
116 ILogger<DmbFactory> logger,
117 Api.Models.Instance metadata)
118 {
119 this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
120 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
121 this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
122 this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
123 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
124 this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
125
126 cleanupTask = Task.CompletedTask;
127 newerDmbTcs = new TaskCompletionSource();
128 cleanupCts = new CancellationTokenSource();
129 jobLockCounts = new Dictionary<long, int>();
130 }
131
133 public void Dispose() => cleanupCts.Dispose(); // we don't dispose nextDmbProvider here, since it might be the only thing we have
134
136 public async ValueTask LoadCompileJob(CompileJob job, Action<bool>? activationAction, CancellationToken cancellationToken)
137 {
138 ArgumentNullException.ThrowIfNull(job);
139
140 var newProvider = await FromCompileJob(job, cancellationToken);
141 if (newProvider == null)
142 return;
143
144 // Do this first, because it's entirely possible when we set the tcs it will immediately need to be applied
145 if (started)
146 {
148 metadata,
149 job);
150 await remoteDeploymentManager.StageDeployment(
151 newProvider.CompileJob,
152 activationAction,
153 cancellationToken);
154 }
155
156 ValueTask dmbDisposeTask;
157 lock (jobLockCounts)
158 {
159 dmbDisposeTask = nextDmbProvider?.DisposeAsync() ?? ValueTask.CompletedTask;
160 nextDmbProvider = newProvider;
161
162 // Oh god dammit
163 var temp = Interlocked.Exchange(ref newerDmbTcs, new TaskCompletionSource());
164 temp.SetResult();
165 }
166
167 await dmbDisposeTask;
168 }
169
171 public IDmbProvider LockNextDmb(int lockCount)
172 {
173 if (!DmbAvailable)
174 throw new InvalidOperationException("No .dmb available!");
175 if (lockCount < 0)
176 throw new ArgumentOutOfRangeException(nameof(lockCount), lockCount, "lockCount must be greater than or equal to 0!");
177 lock (jobLockCounts)
178 {
179 var jobId = nextDmbProvider.CompileJob.Require(x => x.Id);
180 var incremented = jobLockCounts[jobId] += lockCount;
181 logger.LogTrace("Compile job {jobId} lock count now: {lockCount}", jobId, incremented);
182 return nextDmbProvider;
183 }
184 }
185
187 public async Task StartAsync(CancellationToken cancellationToken)
188 {
189 CompileJob? cj = null;
191 async (db) =>
192 cj = await db
193 .CompileJobs
194 .AsQueryable()
195 .Where(x => x.Job.Instance!.Id == metadata.Id)
196 .OrderByDescending(x => x.Job.StoppedAt)
197 .FirstOrDefaultAsync(cancellationToken));
198
199 try
200 {
201 if (cj == default(CompileJob))
202 return;
203 await LoadCompileJob(cj, null, cancellationToken);
204 }
205 finally
206 {
207 started = true;
208 }
209
210 // we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet
211 }
212
214 public async Task StopAsync(CancellationToken cancellationToken)
215 {
216 try
217 {
218 lock (jobLockCounts)
220
221 using (cancellationToken.Register(() => cleanupCts.Cancel()))
222 await cleanupTask;
223 }
224 finally
225 {
226 started = false;
227 }
228 }
229
231#pragma warning disable CA1506 // TODO: Decomplexify
232 public async ValueTask<IDmbProvider?> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
233 {
234 ArgumentNullException.ThrowIfNull(compileJob);
235
236 // ensure we have the entire metadata tree
237 var compileJobId = compileJob.Require(x => x.Id);
238 logger.LogTrace("Loading compile job {id}...", compileJobId);
240 async db => compileJob = await db
241 .CompileJobs
242 .AsQueryable()
243 .Where(x => x!.Id == compileJobId)
244 .Include(x => x.Job!)
245 .ThenInclude(x => x.StartedBy)
246 .Include(x => x.Job!)
247 .ThenInclude(x => x.Instance)
248 .Include(x => x.RevisionInformation!)
249 .ThenInclude(x => x.PrimaryTestMerge!)
250 .ThenInclude(x => x.MergedBy)
251 .Include(x => x.RevisionInformation!)
252 .ThenInclude(x => x.ActiveTestMerges!)
253 .ThenInclude(x => x.TestMerge!)
254 .ThenInclude(x => x.MergedBy)
255 .FirstAsync(cancellationToken)); // can't wait to see that query
256
257 EngineVersion engineVersion;
258 if (!EngineVersion.TryParse(compileJob.EngineVersion, out var engineVersionNullable))
259 {
260 logger.LogWarning("Error loading compile job, bad engine version: {engineVersion}", compileJob.EngineVersion);
261 return null; // omae wa mou shinderu
262 }
263 else
264 engineVersion = engineVersionNullable!;
265
266 if (!compileJob.Job.StoppedAt.HasValue)
267 {
268 // This happens when we're told to load the compile job that is currently finished up
269 // It constitutes an API violation if it's returned by the DreamDaemonController so just set it here
270 // Bit of a hack, but it works out to be nearly if not the same value that's put in the DB
271 logger.LogTrace("Setting missing StoppedAt for CompileJob.Job #{id}...", compileJob.Job.Id);
272 compileJob.Job.StoppedAt = DateTimeOffset.UtcNow;
273 }
274
275 var providerSubmitted = false;
276
277 void CleanupAction()
278 {
279 if (providerSubmitted)
280 CleanRegisteredCompileJob(compileJob);
281 }
282
283 var newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction));
284 try
285 {
286 const string LegacyADirectoryName = "A";
287 const string LegacyBDirectoryName = "B";
288
289 var dmbExistsAtRoot = await ioManager.FileExists(
291 newProvider.Directory,
292 newProvider.DmbName),
293 cancellationToken);
294
295 if (!dmbExistsAtRoot)
296 {
297 logger.LogTrace("Didn't find .dmb at game directory root, checking A/B dirs...");
298 var primaryCheckTask = ioManager.FileExists(
300 newProvider.Directory,
301 LegacyADirectoryName,
302 newProvider.DmbName),
303 cancellationToken);
304 var secondaryCheckTask = ioManager.FileExists(
306 newProvider.Directory,
307 LegacyBDirectoryName,
308 newProvider.DmbName),
309 cancellationToken);
310
311 if (!(await primaryCheckTask && await secondaryCheckTask))
312 {
313 logger.LogWarning("Error loading compile job, .dmb missing!");
314 return null; // omae wa mou shinderu
315 }
316
317 // rebuild the provider because it's using the legacy style directories
318 // Don't dispose it
319 logger.LogDebug("Creating legacy two folder .dmb provider targeting {aDirName} directory...", LegacyADirectoryName);
320 newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction), Path.DirectorySeparatorChar + LegacyADirectoryName);
321 }
322
323 lock (jobLockCounts)
324 {
325 if (!jobLockCounts.TryGetValue(compileJobId, out int value))
326 {
327 value = 1;
328 jobLockCounts.Add(compileJobId, 1);
329 }
330 else
331 jobLockCounts[compileJobId] = ++value;
332
333 providerSubmitted = true;
334
335 logger.LogTrace("Compile job {id} lock count now: {lockCount}", compileJobId, value);
336 return newProvider;
337 }
338 }
339 finally
340 {
341 if (!providerSubmitted)
342 await newProvider.DisposeAsync();
343 }
344 }
345#pragma warning restore CA1506
346
348#pragma warning disable CA1506 // TODO: Decomplexify
349 public async ValueTask CleanUnusedCompileJobs(CancellationToken cancellationToken)
350 {
351 List<long> jobIdsToSkip;
352
353 // don't clean locked directories
354 lock (jobLockCounts)
355 jobIdsToSkip = jobLockCounts.Keys.ToList();
356
357 List<string>? jobUidsToNotErase = null;
358
359 // find the uids of locked directories
360 if (jobIdsToSkip.Count > 0)
361 {
362 await databaseContextFactory.UseContext(async db =>
363 {
364 jobUidsToNotErase = (await db
365 .CompileJobs
366 .AsQueryable()
367 .Where(
368 x => x.Job.Instance!.Id == metadata.Id
369 && jobIdsToSkip.Contains(x.Id!.Value))
370 .Select(x => x.DirectoryName!.Value)
371 .ToListAsync(cancellationToken))
372 .Select(x => x.ToString())
373 .ToList();
374 });
375 }
376 else
377 jobUidsToNotErase = new List<string>();
378
379 jobUidsToNotErase!.Add(SwappableDmbProvider.LiveGameDirectory);
380
381 logger.LogTrace("We will not clean the following directories: {directoriesToNotClean}", String.Join(", ", jobUidsToNotErase));
382
383 // cleanup
384 var gameDirectory = ioManager.ResolvePath();
385 await ioManager.CreateDirectory(gameDirectory, cancellationToken);
386 var directories = await ioManager.GetDirectories(gameDirectory, cancellationToken);
387 int deleting = 0;
388 var tasks = directories.Select(async x =>
389 {
390 var nameOnly = ioManager.GetFileName(x);
391 if (jobUidsToNotErase.Contains(nameOnly))
392 return;
393 logger.LogDebug("Cleaning unused game folder: {dirName}...", nameOnly);
394 try
395 {
396 ++deleting;
397 await DeleteCompileJobContent(x, cancellationToken);
398 }
399 catch (OperationCanceledException)
400 {
401 throw;
402 }
403 catch (Exception e)
404 {
405 logger.LogWarning(e, "Error deleting directory {dirName}!", x);
406 }
407 }).ToList();
408 if (deleting > 0)
409 await Task.WhenAll(tasks);
410 }
411#pragma warning restore CA1506
412
415 {
416 if (!DmbAvailable)
417 return null;
418 return LockNextDmb(0)?.CompileJob;
419 }
420
426 {
427 async Task HandleCleanup()
428 {
429 // First kill the GitHub deployment
431
432 // DCT: None available
433 var deploymentJob = remoteDeploymentManager.MarkInactive(job, CancellationToken.None);
434
435 var deleteTask = DeleteCompileJobContent(job.DirectoryName!.Value.ToString(), cleanupCts.Token);
436 var otherTask = cleanupTask;
437
438 async Task WrapThrowableTasks()
439 {
440 try
441 {
442 await ValueTaskExtensions.WhenAll(deleteTask, deploymentJob);
443 }
444 catch (Exception ex)
445 {
446 logger.LogWarning(ex, "Error cleaning up compile job {jobGuid}!", job.DirectoryName);
447 }
448 }
449
450 await Task.WhenAll(otherTask, WrapThrowableTasks());
451 }
452
453 lock (jobLockCounts)
454 {
455 var jobId = job.Require(x => x.Id);
456 if (jobLockCounts.TryGetValue(jobId, out var currentVal))
457 if (currentVal == 1)
458 {
459 jobLockCounts.Remove(jobId);
460 logger.LogDebug("Cleaning lock-free compile job {id} => {dirName}", jobId, job.DirectoryName);
461 cleanupTask = HandleCleanup();
462 }
463 else
464 {
465 var decremented = --jobLockCounts[jobId];
466 logger.LogTrace("Compile job {id} lock count now: {lockCount}", jobId, decremented);
467 }
468 else
469 logger.LogError("Extra Dispose of DmbProvider for CompileJob {compileJobId}!", jobId);
470 }
471 }
472
479 async ValueTask DeleteCompileJobContent(string directory, CancellationToken cancellationToken)
480 {
481 // Then call the cleanup event, waiting here first
482 await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List<string> { ioManager.ResolvePath(directory) }, true, cancellationToken);
483 await ioManager.DeleteDirectory(directory, cancellationToken);
484 }
485 }
486}
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 .
readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory
The IRemoteDeploymentManagerFactory for the DmbFactory.
Definition: DmbFactory.cs:55
async ValueTask CleanUnusedCompileJobs(CancellationToken cancellationToken)
Deletes all compile jobs that are inactive in the Game folder. A ValueTask representing the running o...
Definition: DmbFactory.cs:349
readonly Dictionary< long, int > jobLockCounts
Map of CompileJob.JobIds to locks on them.
Definition: DmbFactory.cs:80
readonly IIOManager ioManager
The IIOManager for the DmbFactory.
Definition: DmbFactory.cs:50
CompileJob? LatestCompileJob()
Gets the latest CompileJob. The latest CompileJob or null if none are available.
Definition: DmbFactory.cs:414
async ValueTask LoadCompileJob(CompileJob job, Action< bool >? activationAction, CancellationToken cancellationToken)
Load a new job into the ICompileJobSink. A ValueTask representing the running operation.
Definition: DmbFactory.cs:136
readonly IEventConsumer eventConsumer
The IEventConsumer for DmbFactory.
Definition: DmbFactory.cs:65
DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, IEventConsumer eventConsumer, ILogger< DmbFactory > logger, Api.Models.Instance metadata)
Initializes a new instance of the DmbFactory class.
Definition: DmbFactory.cs:111
bool DmbAvailable
If LockNextDmb will succeed.
Definition: DmbFactory.cs:40
volatile TaskCompletionSource newerDmbTcs
TaskCompletionSource resulting in the latest DmbProvider yet to exist.
Definition: DmbFactory.cs:85
readonly ILogger< DmbFactory > logger
The ILogger for the DmbFactory.
Definition: DmbFactory.cs:60
void CleanRegisteredCompileJob(CompileJob job)
Delete the Api.Models.Internal.CompileJob.DirectoryName of job .
Definition: DmbFactory.cs:425
async Task StopAsync(CancellationToken cancellationToken)
Definition: DmbFactory.cs:214
readonly Api.Models.Instance metadata
The Api.Models.Instance for the DmbFactory.
Definition: DmbFactory.cs:70
IDmbProvider? nextDmbProvider
The latest DmbProvider.
Definition: DmbFactory.cs:95
IDmbProvider LockNextDmb(int lockCount)
Gets the next IDmbProvider. DmbAvailable is a precondition. A new IDmbProvider.
Definition: DmbFactory.cs:171
Task OnNewerDmb
Get a Task that completes when the result of a call to LockNextDmb will be different than the previou...
Definition: DmbFactory.cs:30
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the DmbFactory.
Definition: DmbFactory.cs:45
readonly CancellationTokenSource cleanupCts
The CancellationTokenSource for cleanupTask.
Definition: DmbFactory.cs:75
bool started
If the DmbFactory is "started" via IComponentService.
Definition: DmbFactory.cs:100
async ValueTask DeleteCompileJobContent(string directory, CancellationToken cancellationToken)
Handles cleaning the resources of a CompileJob.
Definition: DmbFactory.cs:479
async Task StartAsync(CancellationToken cancellationToken)
Definition: DmbFactory.cs:187
async ValueTask< IDmbProvider?> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
Gets a IDmbProvider for a given CompileJob. A ValueTask<TResult> resulting in a new IDmbProvider repr...
Definition: DmbFactory.cs:232
Task cleanupTask
Task representing calls to CleanRegisteredCompileJob(CompileJob).
Definition: DmbFactory.cs:90
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.
Definition: IDmbProvider.cs:11
Models.CompileJob CompileJob
The CompileJob of the .dmb.
Definition: IDmbProvider.cs:25
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.
EventType
Types of events. Mirror in tgs.dm. Prefer last listed name for script.
Definition: EventType.cs:7