tgstation-server  4.3.2
The /tg/station 13 server suite
JobManager.cs
Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Serilog.Context;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
11 
12 namespace Tgstation.Server.Host.Jobs
13 {
16  {
21 
25  readonly ILogger<JobManager> logger;
26 
30  readonly Dictionary<long, JobHandler> jobs;
31 
35  readonly object synchronizationLock;
36 
42  public JobManager(IDatabaseContextFactory databaseContextFactory, ILogger<JobManager> logger)
43  {
44  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
45  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
46  jobs = new Dictionary<long, JobHandler>();
47  synchronizationLock = new object();
48  }
49 
51  public void Dispose()
52  {
53  foreach (var job in jobs)
54  job.Value.Dispose();
55  }
56 
63  {
64  lock (synchronizationLock)
65  {
66  if (!jobs.TryGetValue(job.Id, out JobHandler jobHandler))
67  throw new InvalidOperationException("Job not running!");
68  return jobHandler;
69  }
70  }
71 
79  async Task RunJob(Job job, Func<Job, IDatabaseContextFactory, CancellationToken, Task> operation, CancellationToken cancellationToken)
80  {
81  using (LogContext.PushProperty("Job", job.Id))
82  try
83  {
84  void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
85  try
86  {
87  var oldJob = job;
88  job = new Job { Id = oldJob.Id };
89 
90  await operation(job, databaseContextFactory, cancellationToken).ConfigureAwait(false);
91 
92  logger.LogDebug("Job {0} completed!", job.Id);
93  }
94  catch (OperationCanceledException)
95  {
96  logger.LogDebug("Job {0} cancelled!", job.Id);
97  job.Cancelled = true;
98  }
99  catch (JobException e)
100  {
101  job.ErrorCode = e.ErrorCode;
102  job.ExceptionDetails = e.Message;
103  LogRegularException();
104  if (e.InnerException != null)
105  logger.LogDebug(
106  "Inner exception for job {0}: {1}",
107  job.Id,
108  e.InnerException is JobException
109  ? e.InnerException.Message
110  : e.InnerException.ToString());
111  }
112  catch (Exception e)
113  {
114  job.ExceptionDetails = e.ToString();
115  LogRegularException();
116  }
117 
118  await databaseContextFactory.UseContext(async databaseContext =>
119  {
120  var attachedJob = new Job
121  {
122  Id = job.Id
123  };
124 
125  databaseContext.Jobs.Attach(attachedJob);
126  attachedJob.StoppedAt = DateTimeOffset.Now;
127  attachedJob.ExceptionDetails = job.ExceptionDetails;
128  attachedJob.ErrorCode = job.ErrorCode;
129  attachedJob.Cancelled = job.Cancelled;
130 
131  await databaseContext.Save(default).ConfigureAwait(false);
132  }).ConfigureAwait(false);
133  }
134  finally
135  {
136  lock (synchronizationLock)
137  {
138  var handler = jobs[job.Id];
139  jobs.Remove(job.Id);
140  handler.Dispose();
141  }
142  }
143  }
144 
146  public Task RegisterOperation(Job job, Func<Job, IDatabaseContextFactory, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
147  {
148  if (job == null)
149  throw new ArgumentNullException(nameof(job));
150  if (operation == null)
151  throw new ArgumentNullException(nameof(operation));
152 
153  job.StartedAt = DateTimeOffset.Now;
154  job.Cancelled = false;
155 
156  job.Instance = new Instance
157  {
158  Id = job.Instance.Id
159  };
160  databaseContext.Instances.Attach(job.Instance);
161 
162  job.StartedBy = new User
163  {
164  Id = job.StartedBy.Id
165  };
166  databaseContext.Users.Attach(job.StartedBy);
167 
168  databaseContext.Jobs.Add(job);
169 
170  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
171  logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description);
172  var jobHandler = new JobHandler(x => RunJob(job, (jobParam, serviceProvider, ct) =>
173  operation(jobParam, serviceProvider, y =>
174  {
175  lock (synchronizationLock)
176  if (jobs.TryGetValue(job.Id, out var handler))
177  handler.Progress = y;
178  }, ct),
179  x));
180  lock (synchronizationLock)
181  jobs.Add(job.Id, jobHandler);
182  });
183 
185  public async Task StartAsync(CancellationToken cancellationToken)
186  {
187  logger.LogTrace("Starting job manager...");
188  await databaseContextFactory.UseContext(async databaseContext =>
189  {
190  // mark all jobs as cancelled
191  var badJobs = await databaseContext
192  .Jobs
193  .AsQueryable()
194  .Where(y => !y.StoppedAt.HasValue)
195  .Select(y => y.Id)
196  .ToListAsync(cancellationToken)
197  .ConfigureAwait(false);
198  if (badJobs.Count > 0)
199  {
200  logger.LogTrace("Cleaning {0} unfinished jobs...", badJobs.Count);
201  foreach (var I in badJobs)
202  {
203  var job = new Job { Id = I };
204  databaseContext.Jobs.Attach(job);
205  job.Cancelled = true;
206  job.StoppedAt = DateTimeOffset.Now;
207  }
208 
209  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
210  }
211  }).ConfigureAwait(false);
212  logger.LogDebug("Job manager started!");
213  }
214 
216  public async Task StopAsync(CancellationToken cancellationToken)
217  {
218  var joinTasks = jobs.Select(x =>
219  {
220  x.Value.Cancel();
221  return x.Value.Wait(cancellationToken);
222  });
223  await Task.WhenAll(joinTasks).ConfigureAwait(false);
224  }
225 
227  public async Task<Job> CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
228  {
229  if (job == null)
230  throw new ArgumentNullException(nameof(job));
231  if (user == null)
232  throw new ArgumentNullException(nameof(user));
233  JobHandler handler;
234  try
235  {
236  handler = CheckGetJob(job);
237  }
238  catch (InvalidOperationException)
239  {
240  // this is fine
241  return null;
242  }
243 
244  handler.Cancel(); // this will ensure the db update is only done once
245  await databaseContextFactory.UseContext(async databaseContext =>
246  {
247  var updatedJob = new Job { Id = job.Id };
248  databaseContext.Jobs.Attach(job);
249  var attachedUser = new User { Id = user.Id };
250  databaseContext.Users.Attach(user);
251  updatedJob.CancelledBy = attachedUser;
252 
253  // let either startup or cancellation set job.cancelled
254  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
255  job.CancelledBy = user;
256  }).ConfigureAwait(false);
257  if (blocking)
258  await handler.Wait(cancellationToken).ConfigureAwait(false);
259  return job;
260  }
261 
263  public int? JobProgress(Job job)
264  {
265  if (job == null)
266  throw new ArgumentNullException(nameof(job));
267  lock (synchronizationLock)
268  {
269  if (!jobs.TryGetValue(job.Id, out var handler))
270  return null;
271  return handler.Progress;
272  }
273  }
274 
276  public async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
277  {
278  if (job == null)
279  throw new ArgumentNullException(nameof(job));
280  if (canceller == null)
281  throw new ArgumentNullException(nameof(canceller));
282  JobHandler handler;
283  lock (synchronizationLock)
284  {
285  if (!jobs.TryGetValue(job.Id, out handler))
286  return;
287  }
288 
289  Task cancelTask = null;
290  using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken)))
291  await handler.Wait(cancellationToken).ConfigureAwait(false);
292 
293  if (cancelTask != null)
294  await cancelTask.ConfigureAwait(false);
295  }
296  }
297 }
long Id
The ID of the entity.
Definition: EntityId.cs:11
readonly IDatabaseContextFactory databaseContextFactory
The IServiceProvider for the JobManager
Definition: JobManager.cs:20
User StartedBy
See Api.Models.Job.StartedBy
Definition: Job.cs:12
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
async Task StartAsync(CancellationToken cancellationToken)
Definition: JobManager.cs:185
readonly Dictionary< long, JobHandler > jobs
Dictionary<TKey, TValue> of Job Api.Models.EntityId.Ids to running JobHandlers
Definition: JobManager.cs:30
DateTimeOffset StartedAt
When the Job was started
Definition: Job.cs:32
string Description
English description of the Job
Definition: Job.cs:16
int JobProgress(Job job)
Get the Api.Models.Job.Progress for a job
Definition: JobManager.cs:263
string ExceptionDetails
Details of any exceptions caught during the Job
Definition: Job.cs:26
Class for pairing Tasks with CancellationTokenSources
Definition: JobHandler.cs:10
User CancelledBy
See Api.Models.Job.CancelledBy
Definition: Job.cs:17
Instance Instance
The Models.Instance the job belongs to if any
Definition: Job.cs:23
async Task< Job > CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
Cancels a give job
Definition: JobManager.cs:227
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:9
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
JobHandler CheckGetJob(Job job)
Gets the JobHandler for a given job if it exists
Definition: JobManager.cs:62
ErrorCode ErrorCode
The Models.ErrorCode associated with the Job if any.
Definition: Job.cs:21
async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
Wait for a given job to complete
Definition: JobManager.cs:276
long Id
The ID of the User
Definition: User.cs:16
JobManager(IDatabaseContextFactory databaseContextFactory, ILogger< JobManager > logger)
Construct a JobManager
Definition: JobManager.cs:42
Manages the runtime of Jobs
Definition: IJobManager.cs:13
readonly object synchronizationLock
object for various operations.
Definition: JobManager.cs:35
async Task RunJob(Job job, Func< Job, IDatabaseContextFactory, CancellationToken, Task > operation, CancellationToken cancellationToken)
Runner for JobHandlers
Definition: JobManager.cs:79
bool Cancelled
If the Job was cancelled
Definition: Job.cs:43
async Task StopAsync(CancellationToken cancellationToken)
Definition: JobManager.cs:216
async Task Wait(CancellationToken cancellationToken)
Wait for task to complete
Definition: JobHandler.cs:47
ErrorCode ErrorCode
The Api.Models.ErrorCode associated with the JobException.
Definition: JobException.cs:14
readonly ILogger< JobManager > logger
The ILogger for the JobManager
Definition: JobManager.cs:25