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