tgstation-server  4.4.0
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.IO;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
10 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host.Components.Deployment
14 {
19  {
21  public Task OnNewerDmb
22  {
23  get
24  {
25  lock (jobLockCounts)
26  return newerDmbTcs.Task;
27  }
28  }
29 
31  public bool DmbAvailable => nextDmbProvider != null;
32 
37 
42 
46  readonly ILogger<DmbFactory> logger;
47 
51  readonly Api.Models.Instance instance;
52 
56  readonly CancellationTokenSource cleanupCts;
57 
61  readonly IDictionary<long, int> jobLockCounts;
62 
67 
71  TaskCompletionSource<object> newerDmbTcs;
72 
77 
85  public DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, ILogger<DmbFactory> logger, Api.Models.Instance instance)
86  {
87  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
88  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
89  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
90  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
91 
92  cleanupTask = Task.CompletedTask;
93  newerDmbTcs = new TaskCompletionSource<object>();
94  cleanupCts = new CancellationTokenSource();
95  jobLockCounts = new Dictionary<long, int>();
96  }
97 
99  public void Dispose() => cleanupCts.Dispose(); // we don't dispose nextDmbProvider here, since it might be the only thing we have
100 
106  {
107  async Task HandleCleanup()
108  {
109  var deleteJob = ioManager.DeleteDirectory(job.DirectoryName.ToString(), cleanupCts.Token);
110  Task otherTask;
111 
112  // lock (this) //already locked below
113  otherTask = cleanupTask;
114  await Task.WhenAll(otherTask, deleteJob).ConfigureAwait(false);
115  }
116 
117  lock (jobLockCounts)
118  if (!jobLockCounts.TryGetValue(job.Id, out var currentVal) || currentVal == 1)
119  {
120  jobLockCounts.Remove(job.Id);
121  logger.LogDebug("Cleaning lock-free compile job {0} => {1}", job.Id, job.DirectoryName);
122  cleanupTask = HandleCleanup();
123  }
124  else
125  {
126  var decremented = --jobLockCounts[job.Id];
127  logger.LogTrace("Compile job {0} lock count now: {1}", job.Id, decremented);
128  }
129  }
130 
132  public async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken)
133  {
134  if (job == null)
135  throw new ArgumentNullException(nameof(job));
136 
137  var newProvider = await FromCompileJob(job, cancellationToken).ConfigureAwait(false);
138  if (newProvider == null)
139  return;
140  lock (jobLockCounts)
141  {
142  nextDmbProvider?.Dispose();
143  nextDmbProvider = newProvider;
144 
145  // Oh god dammit
146  var temp = newerDmbTcs;
147  newerDmbTcs = new TaskCompletionSource<object>();
148  temp.SetResult(nextDmbProvider);
149  }
150  }
151 
153  public IDmbProvider LockNextDmb(int lockCount)
154  {
155  if (!DmbAvailable)
156  throw new InvalidOperationException("No .dmb available!");
157  if (lockCount < 0)
158  throw new ArgumentOutOfRangeException(nameof(lockCount), lockCount, "lockCount must be greater than or equal to 0!");
159  lock (jobLockCounts)
160  {
161  var jobId = nextDmbProvider.CompileJob.Id;
162  var incremented = jobLockCounts[jobId] += lockCount;
163  logger.LogTrace("Compile job {0} lock count now: {1}", jobId, incremented);
164  return nextDmbProvider;
165  }
166  }
167 
169  public async Task StartAsync(CancellationToken cancellationToken)
170  {
171  CompileJob cj = null;
172  await databaseContextFactory.UseContext(async (db) =>
173  {
174  cj = await db
175  .CompileJobs
176  .AsQueryable()
177  .Where(x => x.Job.Instance.Id == instance.Id)
178  .OrderByDescending(x => x.Job.StoppedAt)
179  .FirstOrDefaultAsync(cancellationToken)
180  .ConfigureAwait(false);
181  })
182  .ConfigureAwait(false);
183 
184  if (cj == default(CompileJob))
185  return;
186  await LoadCompileJob(cj, cancellationToken).ConfigureAwait(false);
187 
188  // we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet
189  }
190 
192  public async Task StopAsync(CancellationToken cancellationToken)
193  {
194  using (cancellationToken.Register(() => cleanupCts.Cancel()))
195  await cleanupTask.ConfigureAwait(false);
196  }
197 
199  #pragma warning disable CA1506 // TODO: Decomplexify
200  public async Task<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
201  {
202  if (compileJob == null)
203  throw new ArgumentNullException(nameof(compileJob));
204 
205  // ensure we have the entire compile job tree
206  logger.LogTrace("Loading compile job {0}...", compileJob.Id);
207  await databaseContextFactory.UseContext(
208  async db => compileJob = await db
209  .CompileJobs
210  .AsQueryable()
211  .Where(x => x.Id == compileJob.Id)
212  .Include(x => x.Job).ThenInclude(x => x.StartedBy)
213  .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
214  .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
215  .FirstAsync(cancellationToken)
216  .ConfigureAwait(false))
217  .ConfigureAwait(false); // can't wait to see that query
218 
219  if (!compileJob.Job.StoppedAt.HasValue)
220  {
221  // This happens if we're told to load the compile job that is currently finished up
222  // It can constitute an API violation if it's returned by the DreamDaemonController so just set it here
223  // Bit of a hack, but it should work out to be the same value
224  logger.LogTrace("Setting missing StoppedAt for CompileJob job...");
225  compileJob.Job.StoppedAt = DateTimeOffset.Now;
226  }
227 
228  var providerSubmitted = false;
229 
230  void CleanupAction()
231  {
232  if (providerSubmitted)
233  CleanJob(compileJob);
234  }
235 
236  var newProvider = new DmbProvider(compileJob, ioManager, CleanupAction);
237  try
238  {
239  const string LegacyADirectoryName = "A";
240  const string LegacyBDirectoryName = "B";
241 
242  var dmbExistsAtRoot = await ioManager.FileExists(
243  ioManager.ConcatPath(
244  newProvider.Directory,
245  newProvider.DmbName),
246  cancellationToken)
247  .ConfigureAwait(false);
248 
249  if (!dmbExistsAtRoot)
250  {
251  var primaryCheckTask = ioManager.FileExists(
252  ioManager.ConcatPath(
253  newProvider.Directory,
254  LegacyADirectoryName,
255  newProvider.DmbName),
256  cancellationToken);
257  var secondaryCheckTask = ioManager.FileExists(
258  ioManager.ConcatPath(
259  newProvider.Directory,
260  LegacyBDirectoryName,
261  newProvider.DmbName),
262  cancellationToken);
263 
264  if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
265  {
266  logger.LogWarning("Error loading compile job, .dmb missing!");
267  return null; // omae wa mou shinderu
268  }
269 
270  // rebuild the provider because it's using the legacy style directories
271  // Don't dispose it
272  logger.LogDebug("Creating legacy two folder .dmb provider targeting {0} directory...", LegacyADirectoryName);
273  newProvider = new DmbProvider(compileJob, ioManager, CleanupAction, Path.DirectorySeparatorChar + LegacyADirectoryName);
274  }
275 
276  lock (jobLockCounts)
277  {
278  if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
279  {
280  value = 1;
281  jobLockCounts.Add(compileJob.Id, 1);
282  }
283  else
284  jobLockCounts[compileJob.Id] = ++value;
285 
286  logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value);
287 
288  providerSubmitted = true;
289  return newProvider;
290  }
291  }
292  finally
293  {
294  if (!providerSubmitted)
295  newProvider.Dispose();
296  }
297  }
298  #pragma warning restore CA1506
299 
301  #pragma warning disable CA1506 // TODO: Decomplexify
302  public async Task CleanUnusedCompileJobs(CancellationToken cancellationToken)
303  {
304  List<long> jobIdsToSkip;
305 
306  // don't clean locked directories
307  lock (jobLockCounts)
308  jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList();
309 
310  List<string> jobUidsToNotErase = null;
311 
312  // find the uids of locked directories
313  await databaseContextFactory.UseContext(async db =>
314  {
315  jobUidsToNotErase = (await db
316  .CompileJobs
317  .AsQueryable()
318  .Where(
319  x => x.Job.Instance.Id == instance.Id
320  && jobIdsToSkip.Contains(x.Id))
321  .Select(x => x.DirectoryName.Value)
322  .ToListAsync(cancellationToken)
323  .ConfigureAwait(false))
324  .Select(x => x.ToString())
325  .ToList();
326  }).ConfigureAwait(false);
327 
328  jobUidsToNotErase.Add(SwappableDmbProvider.LiveGameDirectory);
329 
330  logger.LogTrace("We will not clean the following directories: {0}", String.Join(", ", jobUidsToNotErase));
331 
332  // cleanup
333  var gameDirectory = ioManager.ResolvePath();
334  await ioManager.CreateDirectory(gameDirectory, cancellationToken).ConfigureAwait(false);
335  var directories = await ioManager.GetDirectories(gameDirectory, cancellationToken).ConfigureAwait(false);
336  int deleting = 0;
337  var tasks = directories.Select(async x =>
338  {
339  var nameOnly = ioManager.GetFileName(x);
340  if (jobUidsToNotErase.Contains(nameOnly))
341  return;
342  logger.LogDebug("Cleaning unused game folder: {0}...", nameOnly);
343  try
344  {
345  ++deleting;
346  await ioManager.DeleteDirectory(x, cancellationToken).ConfigureAwait(false);
347  }
348  catch (OperationCanceledException)
349  {
350  throw;
351  }
352  catch (Exception e)
353  {
354  logger.LogWarning("Error deleting directory {0}! Exception: {1}", x, e);
355  }
356  }).ToList();
357  if (deleting > 0)
358  await Task.WhenAll(tasks).ConfigureAwait(false);
359  }
360  #pragma warning restore CA1506
361 
364  {
365  if (!DmbAvailable)
366  return null;
367  return LockNextDmb(0)?.CompileJob;
368  }
369  }
370 }
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:24
async Task StartAsync(CancellationToken cancellationToken)
Definition: DmbFactory.cs:169
IDmbProvider nextDmbProvider
The latest DmbProvider
Definition: DmbFactory.cs:76
long Id
The ID of the entity.
Definition: EntityId.cs:11
async Task StopAsync(CancellationToken cancellationToken)
Definition: DmbFactory.cs:192
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the DmbFactory
Definition: DmbFactory.cs:36
DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, ILogger< DmbFactory > logger, Api.Models.Instance instance)
Construct a DmbFactory
Definition: DmbFactory.cs:85
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
async Task CleanUnusedCompileJobs(CancellationToken cancellationToken)
Deletes all compile jobs that are inactive in the Game folder.
Definition: DmbFactory.cs:302
DateTimeOffset StoppedAt
When the Job stopped
Definition: Job.cs:37
void CleanJob(CompileJob job)
Delete the Api.Models.Internal.CompileJob.DirectoryName of job
Definition: DmbFactory.cs:105
Job Job
See Api.Models.CompileJob.Job
Definition: CompileJob.cs:13
TaskCompletionSource< object > newerDmbTcs
TaskCompletionSource<TResult> resulting in the latest DmbProvider yet to exist
Definition: DmbFactory.cs:71
readonly IDictionary< long, int > jobLockCounts
Map of CompileJob.JobIds to locks on them.
Definition: DmbFactory.cs:61
readonly IIOManager ioManager
The IIOManager for the DmbFactory
Definition: DmbFactory.cs:41
readonly CancellationTokenSource cleanupCts
The CancellationTokenSource for cleanupTask
Definition: DmbFactory.cs:56
CompileJob LatestCompileJob()
Gets the latest CompileJob.
Definition: DmbFactory.cs:363
Guid DirectoryName
The Game folder the results were compiled into
Definition: CompileJob.cs:28
async Task< IDmbProvider > FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
Gets a IDmbProvider for a given CompileJob
Definition: DmbFactory.cs:200
Provides absolute paths to the latest compiled .dmbs
Definition: IDmbProvider.cs:9
Interface for using filesystems
Definition: IIOManager.cs:11
async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken)
Load a new job into the ICompileJobSink
Definition: DmbFactory.cs:132
const string LiveGameDirectory
The directory where the baseProvider is symlinked to.
IDmbProvider LockNextDmb(int lockCount)
Gets the next IDmbProvider
Definition: DmbFactory.cs:153
readonly ILogger< DmbFactory > logger
The ILogger for the DmbFactory
Definition: DmbFactory.cs:46
readonly Api.Models.Instance instance
The Api.Models.Instance for the DmbFactory
Definition: DmbFactory.cs:51
Task cleanupTask
Task representing calls to CleanJob(CompileJob)
Definition: DmbFactory.cs:66