tgstation-server  4.3.2
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;
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  .MostRecentCompletedCompileJobOrDefault(instance, cancellationToken)
176  .ConfigureAwait(false);
177  })
178  .ConfigureAwait(false);
179 
180  if (cj == default(CompileJob))
181  return;
182  await LoadCompileJob(cj, cancellationToken).ConfigureAwait(false);
183 
184  // we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet
185  }
186 
188  public async Task StopAsync(CancellationToken cancellationToken)
189  {
190  using (cancellationToken.Register(() => cleanupCts.Cancel()))
191  await cleanupTask.ConfigureAwait(false);
192  }
193 
195  #pragma warning disable CA1506 // TODO: Decomplexify
196  public async Task<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
197  {
198  if (compileJob == null)
199  throw new ArgumentNullException(nameof(compileJob));
200 
201  // ensure we have the entire compile job tree
202  logger.LogTrace("Loading compile job {0}...", compileJob.Id);
203  await databaseContextFactory.UseContext(
204  async db => compileJob = await db
205  .CompileJobs
206  .AsQueryable()
207  .Where(x => x.Id == compileJob.Id)
208  .Include(x => x.Job).ThenInclude(x => x.StartedBy)
209  .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
210  .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
211  .FirstAsync(cancellationToken)
212  .ConfigureAwait(false))
213  .ConfigureAwait(false); // can't wait to see that query
214 
215  if (!compileJob.Job.StoppedAt.HasValue)
216  {
217  // This happens if we're told to load the compile job that is currently finished up
218  // It can constitute an API violation if it's returned by the DreamDaemonController so just set it here
219  // Bit of a hack, but it should work out to be the same value
220  logger.LogTrace("Setting missing StoppedAt for CompileJob job...");
221  compileJob.Job.StoppedAt = DateTimeOffset.Now;
222  }
223 
224  var providerSubmitted = false;
225  var newProvider = new DmbProvider(compileJob, ioManager, () =>
226  {
227  if (providerSubmitted)
228  CleanJob(compileJob);
229  });
230 
231  try
232  {
233  var primaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
234  var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
235 
236  if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
237  {
238  logger.LogWarning("Error loading compile job, .dmb missing!");
239  return null; // omae wa mou shinderu
240  }
241 
242  lock (jobLockCounts)
243  {
244  if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
245  {
246  value = 1;
247  jobLockCounts.Add(compileJob.Id, 1);
248  }
249  else
250  jobLockCounts[compileJob.Id] = ++value;
251 
252  logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value);
253 
254  providerSubmitted = true;
255  return newProvider;
256  }
257  }
258  finally
259  {
260  if (!providerSubmitted)
261  newProvider.Dispose();
262  }
263  }
264  #pragma warning restore CA1506
265 
267  #pragma warning disable CA1506 // TODO: Decomplexify
268  public async Task CleanUnusedCompileJobs(CancellationToken cancellationToken)
269  {
270  List<long> jobIdsToSkip;
271 
272  // don't clean locked directories
273  lock (jobLockCounts)
274  jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList();
275 
276  List<string> jobUidsToNotErase = null;
277 
278  // find the uids of locked directories
279  await databaseContextFactory.UseContext(async db =>
280  {
281  jobUidsToNotErase = (await db
282  .CompileJobs
283  .AsQueryable()
284  .Where(
285  x => x.Job.Instance.Id == instance.Id
286  && jobIdsToSkip.Contains(x.Id))
287  .Select(x => x.DirectoryName.Value)
288  .ToListAsync(cancellationToken)
289  .ConfigureAwait(false))
290  .Select(x => x.ToString())
291  .ToList();
292  }).ConfigureAwait(false);
293 
294  jobUidsToNotErase.Add(WindowsSwappableDmbProvider.LiveGameDirectory);
295 
296  logger.LogTrace("We will not clean the following directories: {0}", String.Join(", ", jobUidsToNotErase));
297 
298  // cleanup
299  var gameDirectory = ioManager.ResolvePath();
300  await ioManager.CreateDirectory(gameDirectory, cancellationToken).ConfigureAwait(false);
301  var directories = await ioManager.GetDirectories(gameDirectory, cancellationToken).ConfigureAwait(false);
302  int deleting = 0;
303  var tasks = directories.Select(async x =>
304  {
305  var nameOnly = ioManager.GetFileName(x);
306  if (jobUidsToNotErase.Contains(nameOnly))
307  return;
308  logger.LogDebug("Cleaning unused game folder: {0}...", nameOnly);
309  try
310  {
311  ++deleting;
312  await ioManager.DeleteDirectory(x, cancellationToken).ConfigureAwait(false);
313  }
314  catch (OperationCanceledException)
315  {
316  throw;
317  }
318  catch (Exception e)
319  {
320  logger.LogWarning("Error deleting directory {0}! Exception: {1}", x, e);
321  }
322  }).ToList();
323  if (deleting > 0)
324  await Task.WhenAll(tasks).ConfigureAwait(false);
325  }
326  #pragma warning restore CA1506
327 
330  {
331  if (!DmbAvailable)
332  return null;
333  return LockNextDmb(0)?.CompileJob;
334  }
335  }
336 }
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
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:188
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:268
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:329
Guid DirectoryName
The Game folder the results were compiled into
Definition: CompileJob.cs:28
const string LiveGameDirectory
The directory where the baseProvider is symlinked to.
async Task< IDmbProvider > FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
Gets a IDmbProvider for a given CompileJob
Definition: DmbFactory.cs:196
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
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