/// <summary> /// Creates a new background operation. The background operations runs the given method at given intervals. Must be separately started. /// </summary> /// <param name="name">The name of the operation.</param> /// <param name="method">The method to invoke at given intervals.</param> /// <param name="options">The options for the display of the background operation in the dashboard.</param> /// <returns>A new background operation, that is not yet started.</returns> public TaskQueueBackgroundOperation <TDirective, TSecureConfiguration> CreateBackgroundOperation <TDirective> ( string name, Action <TaskProcessorJobEx <BackgroundOperationTaskDirective, TSecureConfiguration>, TDirective> method ) where TDirective : TaskDirective { TaskQueueBackgroundOperation <TDirective, TSecureConfiguration> backgroundOperation; lock (TaskQueueBackgroundOperationManager <TSecureConfiguration> ._lock) { if (this.BackgroundOperations.ContainsKey(name)) { throw new ArgumentException( $"A background operation with the name {name} in queue {this.QueueId} already exists.", nameof(name)); } // Create the background operation. backgroundOperation = new TaskQueueBackgroundOperation <TDirective, TSecureConfiguration> ( this, name, method, this.CancellationTokenSource ); // Add it to the dictionary. this.BackgroundOperations.Add(name, backgroundOperation); } // Return it. return(backgroundOperation); }
/// <summary> /// Handles when a job from a task requires processing. /// Note that all tasks/jobs in the queue use the same task type ID, /// so the directive <see cref="BackgroundOperationTaskDirective.BackgroundOperationName"/> /// is used to identify which tasks should be executed by /// which background operation. /// </summary> /// <param name="job">The job to process.</param> protected virtual void ProcessJobHandler(AppTasks.ITaskProcessingJob <BackgroundOperationTaskDirective> job) { // Sanity. if (null == job) { return; } // Ensure cancellation has not been requested. job.ThrowIfTaskCancellationRequested(); // What is the current state? var state = job.GetStatus(); // Sanity. if (null == job.Directive) { // This is an issue. We have no way to decide what background operation should run it. Die. SysUtils.ReportErrorToEventLog ( $"Job loaded with no directive (queue: {this.QueueId}, task type: {job.TaskInfo.TaskType}, task id: {job.TaskInfo.TaskID})." ); return; } // Check that we know the job this was associated with. if (string.IsNullOrWhiteSpace(job.Directive.BackgroundOperationName)) { // This is an issue. We have no way to decide what background operation should run it. Die. SysUtils.ReportErrorToEventLog ( $"Job loaded with no background operation name loaded (queue: {this.QueueId}, task type: {job.TaskInfo.TaskType}, task id: {job.TaskInfo.TaskID})." ); return; } // If we have a directive then extract it. var dir = job.Directive.GetParsedInternalDirective(); // Find the background operation to run. TaskQueueBackgroundOperation <TSecureConfiguration> bo = null; lock (_lock) { if (false == this.BackgroundOperations.TryGetValue(job.Directive.BackgroundOperationName, out bo)) { // We have no registered background operation to handle the callback. SysUtils.ReportErrorToEventLog ( $"No background operation found with name {job.Directive.BackgroundOperationName} (queue: {this.QueueId}, task type: {job.TaskInfo.TaskType}, task id: {job.TaskInfo.TaskID})." ); return; } } // Should we repeat? DateTime?nextRun = null; switch (bo.RepeatType) { case TaskQueueBackgroundOperationRepeatType.Interval: // Add the interval to the current datetime. if (bo.Interval.HasValue) { nextRun = DateTime.UtcNow.Add(bo.Interval.Value); } break; case TaskQueueBackgroundOperationRepeatType.Schedule: // Get the next execution time from the schedule. nextRun = bo.Schedule?.GetNextExecution(DateTime.Now); break; } // If we have a next run time then re-run. if (null != nextRun) { // Bind to the completed event ( called always ) of the job. // That way even if the job is canceled, fails, or finishes successfully // ...we always schedule the next run. job.Completed += (s, op) => { // Ensure that if two threads both run this at once we don't end up with a race condition. lock (_lock) { // Cancel any future executions (we only want the single one created below). this.CancelFutureExecutions(bo.Name); // Now schedule it to run according to the interval. this.RunOnce ( bo.Name, nextRun.Value.ToUniversalTime(), dir ); } }; } // Bind to the life-cycle events. job.Succeeded += (sender, op) => CompleteJob(job, MFTaskState.MFTaskStateCompleted); job.Failed += (sender, args) => CompleteJob(job, MFTaskState.MFTaskStateFailed, args.Exception); // Perform the action. // Mark it as started. job.Update ( new TaskInformation() { Started = DateTime.Now, LastActivity = DateTime.Now, CurrentTaskState = MFTaskState.MFTaskStateInProgress } ); // NOTE: this should not have any error handling around it here unless it re-throws the error // Catching the exception here can result in job.ProcessingFailed not being called // Delegate to the background operation. bo.RunJob ( // The TaskProcessorJobEx class wraps the job and allows easy updates. new TaskProcessorJobEx <BackgroundOperationTaskDirective, TSecureConfiguration>() { Job = job, TaskQueueBackgroundOperationManager = this }, dir ); }