using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Hosting;
using Umbraco.Core.Logging;
using Umbraco.Core.Events;
namespace Umbraco.Web.Scheduling
{
///
/// Manages a queue of tasks of type and runs them in the background.
///
/// The type of the managed tasks.
/// The task runner is web-aware and will ensure that it shuts down correctly when the AppDomain
/// shuts down (ie is unloaded).
internal class BackgroundTaskRunner : IBackgroundTaskRunner
where T : class, IBackgroundTask
{
private readonly BackgroundTaskRunnerOptions _options;
private readonly BlockingCollection _tasks = new BlockingCollection();
private readonly object _locker = new object();
private readonly ManualResetEventSlim _completedEvent = new ManualResetEventSlim(false);
private BackgroundTaskRunnerAwaiter _awaiter;
private volatile bool _isRunning; // is running
private volatile bool _isCompleted; // does not accept tasks anymore, may still be running
private Task _runningTask;
private CancellationTokenSource _tokenSource;
internal event TypedEventHandler, TaskEventArgs> TaskError;
internal event TypedEventHandler, TaskEventArgs> TaskStarting;
internal event TypedEventHandler, TaskEventArgs> TaskCompleted;
internal event TypedEventHandler, TaskEventArgs> TaskCancelled;
internal event TypedEventHandler, EventArgs> Completed;
///
/// Initializes a new instance of the class.
///
public BackgroundTaskRunner()
: this(new BackgroundTaskRunnerOptions())
{ }
///
/// Initializes a new instance of the class with a set of options.
///
/// The set of options.
public BackgroundTaskRunner(BackgroundTaskRunnerOptions options)
{
if (options == null) throw new ArgumentNullException("options");
_options = options;
HostingEnvironment.RegisterObject(this);
if (options.AutoStart)
StartUp();
}
///
/// Gets the number of tasks in the queue.
///
public int TaskCount
{
get { return _tasks.Count; }
}
///
/// Gets a value indicating whether a task is currently running.
///
public bool IsRunning
{
get { return _isRunning; }
}
///
/// Gets a value indicating whether the runner has completed and cannot accept tasks anymore.
///
public bool IsCompleted
{
get { return _isCompleted; }
}
///
/// Gets an awaiter used to await the running Threading.Task.
///
/// There is no running task.
///
/// Unless the AutoStart option is true, there will be no running task until
/// a background task is added to the queue. Unless the KeepAlive option is true, there
/// will be no running task when the queue is empty.
///
public ThreadingTaskAwaiter CurrentThreadingTask
{
get
{
if (_runningTask == null)
throw new InvalidOperationException("There is no current Threading.Task.");
return new ThreadingTaskAwaiter(_runningTask);
}
}
///
/// Gets an awaiter used to await the BackgroundTaskRunner running operation
///
/// An awaiter for the BackgroundTaskRunner running operation
///
/// This is used to wait until the background task runner is no longer running (IsRunning == false)
///
/// So long as we have a method called GetAwaiter() that returns an instance of INotifyCompletion
/// we can await anything. In this case we are awaiting with a custom BackgroundTaskRunnerAwaiter
/// which waits for the Completed event to be raised.
/// ref: http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx
///
///
public BackgroundTaskRunnerAwaiter GetAwaiter()
{
return _awaiter ?? (_awaiter = new BackgroundTaskRunnerAwaiter(this));
}
///
/// Adds a task to the queue.
///
/// The task to add.
/// The task runner has completed.
public void Add(T task)
{
lock (_locker)
{
if (_isCompleted)
throw new InvalidOperationException("The task runner has completed.");
// add task
LogHelper.Debug>("Task added {0}", task.GetType);
_tasks.Add(task);
// start
StartUpLocked();
}
}
///
/// Tries to add a task to the queue.
///
/// The task to add.
/// true if the task could be added to the queue; otherwise false.
/// Returns false if the runner is completed.
public bool TryAdd(T task)
{
lock (_locker)
{
if (_isCompleted) return false;
// add task
LogHelper.Debug>("Task added {0}", task.GetType);
_tasks.Add(task);
// start
StartUpLocked();
return true;
}
}
///
/// Starts the tasks runner, if not already running.
///
/// Is invoked each time a task is added, to ensure it is going to be processed.
/// The task runner has completed.
public void StartUp()
{
if (_isRunning) return;
lock (_locker)
{
if (_isCompleted)
throw new InvalidOperationException("The task runner has completed.");
StartUpLocked();
}
}
///
/// Starts the tasks runner, if not already running.
///
/// Must be invoked within lock(_locker) and with _isCompleted being false.
private void StartUpLocked()
{
// double check
if (_isRunning) return;
_isRunning = true;
// create a new token source since this is a new process
_tokenSource = new CancellationTokenSource();
_runningTask = PumpIBackgroundTasks(Task.Factory, _tokenSource.Token);
LogHelper.Debug>("Starting");
}
///
/// Shuts the taks runner down.
///
/// True for force the runner to stop.
/// True to wait until the runner has stopped.
/// If is false, no more tasks can be queued but all queued tasks
/// will run. If it is true, then only the current one (if any) will end and no other task will run.
public void Shutdown(bool force, bool wait)
{
lock (_locker)
{
_isCompleted = true; // do not accept new tasks
if (_isRunning == false) return; // done already
}
// try to be nice
// assuming multiple threads can do these without problems
_completedEvent.Set();
_tasks.CompleteAdding();
if (force)
{
// we must bring everything down, now
Thread.Sleep(100); // give time to CompleAdding()
lock (_locker)
{
// was CompleteAdding() enough?
if (_isRunning == false) return;
}
// try to cancel running async tasks (cannot do much about sync tasks)
// break delayed tasks delay
// truncate running queues
_tokenSource.Cancel(false); // false is the default
}
// tasks in the queue will be executed...
if (wait == false) return;
_runningTask.Wait(); // wait for whatever is running to end...
}
///
/// Runs background tasks for as long as there are background tasks in the queue, with an asynchronous operation.
///
/// The supporting .
/// A cancellation token.
/// The asynchronous operation.
private Task PumpIBackgroundTasks(TaskFactory factory, CancellationToken token)
{
var taskSource = new TaskCompletionSource