tgstation-server
The /tg/station 13 server suite
DmbFactory.cs
Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
9 using Tgstation.Server.Host.IO;
11 
12 namespace Tgstation.Server.Host.Components.Compiler
13 {
18  {
20  public Task OnNewerDmb
21  {
22  get
23  {
24  lock (this)
25  return newerDmbTcs.Task;
26  }
27  }
28 
30  public bool DmbAvailable => nextDmbProvider != null;
31 
43  readonly ILogger<DmbFactory> logger;
44 
48  readonly Api.Models.Instance instance;
52  readonly CancellationTokenSource cleanupCts;
53 
61  TaskCompletionSource<object> newerDmbTcs;
66 
67  Dictionary<long, int> jobLockCounts;
68 
76  public DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, ILogger<DmbFactory> logger, Api.Models.Instance instance)
77  {
78  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
79  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
80  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
81  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
82 
83  cleanupTask = Task.CompletedTask;
84  newerDmbTcs = new TaskCompletionSource<object>();
85  cleanupCts = new CancellationTokenSource();
86  jobLockCounts = new Dictionary<long, int>();
87  }
88 
90  public void Dispose() => cleanupCts.Dispose(); //we don't dispose nextDmbProvider here, since it might be the only thing we have
91 
96  void CleanJob(CompileJob job)
97  {
98  async Task HandleCleanup()
99  {
100  var deleteJob = ioManager.DeleteDirectory(job.DirectoryName.ToString(), cleanupCts.Token);
101  Task otherTask;
102  //lock (this) //already locked below
103  otherTask = cleanupTask;
104  await Task.WhenAll(otherTask, deleteJob).ConfigureAwait(false);
105  }
106  lock (this)
107  {
108  if (!jobLockCounts.TryGetValue(job.Id, out var currentVal) || currentVal == 1)
109  {
110  jobLockCounts.Remove(job.Id);
111  logger.LogDebug("Cleaning compile job {0} => {1}", job.Id, job.DirectoryName);
112  cleanupTask = HandleCleanup();
113  }
114  else
115  {
116  var decremented = --jobLockCounts[job.Id];
117  logger.LogTrace("Compile job {0} lock count now: {1}", job.Id, decremented);
118  }
119  }
120  }
121 
123  public async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken)
124  {
125  if (job == null)
126  throw new ArgumentNullException(nameof(job));
127 
128  var newProvider = await FromCompileJob(job, cancellationToken).ConfigureAwait(false);
129  if (newProvider == null)
130  return;
131  lock (this)
132  {
133  nextDmbProvider?.Dispose();
134  nextDmbProvider = newProvider;
135  newerDmbTcs.SetResult(nextDmbProvider);
136  newerDmbTcs = new TaskCompletionSource<object>();
137  }
138  }
139 
141  public IDmbProvider LockNextDmb(int lockCount)
142  {
143  if (!DmbAvailable)
144  throw new InvalidOperationException("No .dmb available!");
145  if (lockCount < 0)
146  throw new ArgumentOutOfRangeException(nameof(lockCount), lockCount, "lockCount must be greater than or equal to 0!");
147  lock (this)
148  {
149  var jobId = nextDmbProvider.CompileJob.Id;
150  var incremented = jobLockCounts[jobId] += lockCount;
151  logger.LogTrace("Compile job {0} lock count now: {1}", jobId, incremented);
152  return nextDmbProvider;
153  }
154  }
155 
157  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) =>
158  {
159  //where complete clause not necessary, only successful COMPILEjobs get in the db
160  var cj = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id)
161  .OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
162  if (cj == default(CompileJob))
163  return;
164  await LoadCompileJob(cj, cancellationToken).ConfigureAwait(false);
165  //we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet
166  });
167 
169  public async Task StopAsync(CancellationToken cancellationToken)
170  {
171  using (cancellationToken.Register(() => cleanupCts.Cancel()))
172  await cleanupTask.ConfigureAwait(false);
173  }
174 
176  public async Task<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
177  {
178  if (compileJob == null)
179  throw new ArgumentNullException(nameof(compileJob));
180 
181  //ensure we have the entire compile job tree
182  await databaseContextFactory.UseContext(async db => compileJob = await db.CompileJobs.Where(x => x.Id == compileJob.Id)
183  .Include(x => x.Job).ThenInclude(x => x.StartedBy)
184  .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
185  .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
186  .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); //can't wait to see that query
187 
188  logger.LogTrace("Loading compile job {0}...", compileJob.Id);
189  var providerSubmitted = false;
190  var newProvider = new DmbProvider(compileJob, ioManager, () =>
191  {
192  if (providerSubmitted)
193  CleanJob(compileJob);
194  });
195 
196  try
197  {
198  var primaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
199  var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
200 
201  if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
202  {
203  logger.LogWarning("Error loading compile job, .dmb missing!");
204  return null; //omae wa mou shinderu
205  }
206 
207  lock (this)
208  {
209  if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
210  {
211  value = 1;
212  jobLockCounts.Add(compileJob.Id, 1);
213  }
214  else
215  jobLockCounts[compileJob.Id] = ++value;
216 
217  logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value);
218 
219  providerSubmitted = true;
220  return newProvider;
221  }
222  }
223  finally
224  {
225  if (!providerSubmitted)
226  newProvider.Dispose();
227  }
228  }
229 
231  public async Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken)
232  {
233  List<long> jobIdsToSkip;
234  //don't clean locked directories
235  lock (this)
236  jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList();
237 
238  List<string> jobUidsToNotErase = null;
239 
240  //find the uids of locked directories
241  await databaseContextFactory.UseContext(async db =>
242  {
243  jobUidsToNotErase = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id)).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false);
244  }).ConfigureAwait(false);
245 
246  //add the other exemption
247  if (exceptThisOne != null)
248  jobUidsToNotErase.Add(exceptThisOne.DirectoryName.Value.ToString().ToUpperInvariant());
249 
250  //cleanup
251  await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);
252  var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
253  int deleting = 0;
254  var tasks = directories.Select(async x =>
255  {
256  var nameOnly = ioManager.GetFileName(x);
257  if (jobUidsToNotErase.Contains(nameOnly.ToUpperInvariant()))
258  return;
259  try
260  {
261  ++deleting;
262  await ioManager.DeleteDirectory(x, cancellationToken).ConfigureAwait(false);
263  }
264  catch (OperationCanceledException)
265  {
266  throw;
267  }
268  catch (Exception e)
269  {
270  logger.LogWarning("Error deleting directory {0}! Exception: {1}", x, e);
271  }
272  }).ToList();
273  if (deleting > 0)
274  {
275  logger.LogDebug("Cleaning {0} unused game folders...", deleting);
276  await Task.WhenAll().ConfigureAwait(false);
277  }
278  }
279  }
280 }
IDmbProvider nextDmbProvider
The latest DmbProvider
Definition: DmbFactory.cs:65
Task cleanupTask
Task representing calls to CleanJob(CompileJob)
Definition: DmbFactory.cs:57
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
async Task StopAsync(CancellationToken cancellationToken)
Definition: DmbFactory.cs:169
readonly IIOManager ioManager
The IIOManager for the DmbFactory
Definition: DmbFactory.cs:39
async Task< IDmbProvider > FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
Gets a IDmbProvider for a given CompileJob
Definition: DmbFactory.cs:176
DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, ILogger< DmbFactory > logger, Api.Models.Instance instance)
Construct a DmbFactory
Definition: DmbFactory.cs:76
async Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken)
Deletes all compile jobs that are inactive in the Game folder exceptThisOne
Definition: DmbFactory.cs:231
readonly CancellationTokenSource cleanupCts
The CancellationTokenSource for cleanupTask
Definition: DmbFactory.cs:52
readonly ILogger< DmbFactory > logger
The ILogger for the DmbFactory
Definition: DmbFactory.cs:43
readonly Api.Models.Instance instance
The Api.Models.Instance for the DmbFactory
Definition: DmbFactory.cs:48
void CleanJob(CompileJob job)
Delete the Api.Models.Internal.CompileJob.DirectoryName of job
Definition: DmbFactory.cs:96
IDmbProvider LockNextDmb(int lockCount)
Gets the next IDmbProvider
Definition: DmbFactory.cs:141
Guid DirectoryName
The Game folder the results were compiled into
Definition: CompileJob.cs:32
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the DmbFactory
Definition: DmbFactory.cs:35
TaskCompletionSource< object > newerDmbTcs
TaskCompletionSource<TResult> resulting in the latest DmbProvider yet to exist
Definition: DmbFactory.cs:61
async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken)
Load a new job into the ICompileJobConsumer
Definition: DmbFactory.cs:123
Provides absolute paths to the latest compiled .dmbs
Definition: IDmbProvider.cs:9
Interface for using filesystems
Definition: IIOManager.cs:11