1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
5 using System.Collections.Generic;
8 using System.Threading.Tasks;
30 readonly Dictionary<long, JobHandler>
jobs;
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();
53 foreach (var job
in jobs)
64 lock (synchronizationLock)
66 if (!jobs.TryGetValue(job.
Id, out
JobHandler jobHandler))
67 throw new InvalidOperationException(
"Job not running!");
79 async Task
RunJob(
Job job, Func<Job, IDatabaseContextFactory, CancellationToken, Task> operation, CancellationToken cancellationToken)
81 using (LogContext.PushProperty(
"Job", job.
Id))
84 void LogRegularException() => logger.LogDebug(
"Job {0} exited with error! Exception: {1}", job.
Id, job.
ExceptionDetails);
88 job =
new Job { Id = oldJob.
Id };
90 await operation(job, databaseContextFactory, cancellationToken).ConfigureAwait(
false);
92 logger.LogDebug(
"Job {0} completed!", job.
Id);
94 catch (OperationCanceledException)
96 logger.LogDebug(
"Job {0} cancelled!", job.
Id);
103 LogRegularException();
104 if (e.InnerException != null)
106 "Inner exception for job {0}: {1}",
109 ? e.InnerException.Message
110 : e.InnerException.ToString());
115 LogRegularException();
118 await databaseContextFactory.UseContext(async databaseContext =>
120 var attachedJob =
new Job 125 databaseContext.Jobs.Attach(attachedJob);
126 attachedJob.StoppedAt = DateTimeOffset.Now;
131 await databaseContext.Save(
default).ConfigureAwait(
false);
132 }).ConfigureAwait(
false);
136 lock (synchronizationLock)
138 var handler = jobs[job.
Id];
146 public Task RegisterOperation(
Job job, Func<
Job,
IDatabaseContextFactory, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
149 throw new ArgumentNullException(nameof(job));
150 if (operation == null)
151 throw new ArgumentNullException(nameof(operation));
160 databaseContext.Instances.Attach(job.
Instance);
166 databaseContext.Users.Attach(job.
StartedBy);
168 databaseContext.Jobs.Add(job);
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 =>
175 lock (synchronizationLock)
176 if (jobs.TryGetValue(job.
Id, out var handler))
177 handler.Progress = y;
180 lock (synchronizationLock)
181 jobs.Add(job.
Id, jobHandler);
185 public async Task
StartAsync(CancellationToken cancellationToken)
187 logger.LogTrace(
"Starting job manager...");
188 await databaseContextFactory.UseContext(async databaseContext =>
191 var badJobs = await databaseContext
194 .Where(y => !y.StoppedAt.HasValue)
196 .ToListAsync(cancellationToken)
197 .ConfigureAwait(
false);
198 if (badJobs.Count > 0)
200 logger.LogTrace(
"Cleaning {0} unfinished jobs...", badJobs.Count);
201 foreach (var I
in badJobs)
203 var job =
new Job { Id = I };
204 databaseContext.Jobs.Attach(job);
206 job.StoppedAt = DateTimeOffset.Now;
209 await databaseContext.Save(cancellationToken).ConfigureAwait(
false);
211 }).ConfigureAwait(
false);
212 logger.LogDebug(
"Job manager started!");
216 public async Task
StopAsync(CancellationToken cancellationToken)
218 var joinTasks = jobs.Select(x =>
221 return x.Value.Wait(cancellationToken);
223 await Task.WhenAll(joinTasks).ConfigureAwait(
false);
227 public async Task<Job>
CancelJob(
Job job,
User user,
bool blocking, CancellationToken cancellationToken)
230 throw new ArgumentNullException(nameof(job));
232 throw new ArgumentNullException(nameof(user));
236 handler = CheckGetJob(job);
238 catch (InvalidOperationException)
245 await databaseContextFactory.UseContext(async databaseContext =>
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;
254 await databaseContext.Save(cancellationToken).ConfigureAwait(
false);
256 }).ConfigureAwait(
false);
258 await handler.
Wait(cancellationToken).ConfigureAwait(
false);
266 throw new ArgumentNullException(nameof(job));
267 lock (synchronizationLock)
269 if (!jobs.TryGetValue(job.
Id, out var handler))
271 return handler.Progress;
279 throw new ArgumentNullException(nameof(job));
280 if (canceller == null)
281 throw new ArgumentNullException(nameof(canceller));
283 lock (synchronizationLock)
285 if (!jobs.TryGetValue(job.
Id, out handler))
289 Task cancelTask = null;
290 using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller,
true, cancellationToken)))
291 await handler.
Wait(cancellationToken).ConfigureAwait(
false);
293 if (cancelTask != null)
294 await cancelTask.ConfigureAwait(
false);
long Id
The ID of the entity.
readonly IDatabaseContextFactory databaseContextFactory
The IServiceProvider for the JobManager
User StartedBy
See Api.Models.Job.StartedBy
Use server authentication
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
async Task StartAsync(CancellationToken cancellationToken)
readonly Dictionary< long, JobHandler > jobs
Dictionary<TKey, TValue> of Job Api.Models.EntityId.Ids to running JobHandlers
DateTimeOffset StartedAt
When the Job was started
string Description
English description of the Job
int JobProgress(Job job)
Get the Api.Models.Job.Progress for a job
string ExceptionDetails
Details of any exceptions caught during the Job
Class for pairing Tasks with CancellationTokenSources
User CancelledBy
See Api.Models.Job.CancelledBy
Instance Instance
The Models.Instance the job belongs to if any
async Task< Job > CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
Cancels a give job
Operation exceptions thrown from the context of a Models.Job
Represents an Api.Models.Instance in the database
void Cancel()
Cancels task
JobHandler CheckGetJob(Job job)
Gets the JobHandler for a given job if it exists
ErrorCode ErrorCode
The Models.ErrorCode associated with the Job if any.
async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
Wait for a given job to complete
long Id
The ID of the User
JobManager(IDatabaseContextFactory databaseContextFactory, ILogger< JobManager > logger)
Construct a JobManager
Manages the runtime of Jobs
readonly object synchronizationLock
object for various operations.
async Task RunJob(Job job, Func< Job, IDatabaseContextFactory, CancellationToken, Task > operation, CancellationToken cancellationToken)
Runner for JobHandlers
bool Cancelled
If the Job was cancelled
async Task StopAsync(CancellationToken cancellationToken)
async Task Wait(CancellationToken cancellationToken)
Wait for task to complete
ErrorCode ErrorCode
The Api.Models.ErrorCode associated with the JobException.
readonly ILogger< JobManager > logger
The ILogger for the JobManager