using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Neo.Afx.Services
{
///
/// A scheduler.
///
public class ServiceTaskScheduler
{
///
/// Raised when a task is completed.
///
public event EventHandler TaskCompleted;
///
/// Raised when a task is busy.
///
public event EventHandler TaskBusy;
///
/// Raised when an unhandled error is thrown.
///
public event EventHandler UnhandledError;
///
/// This is here to enhance accuracy. Even if nothing is scheduled the timer sleeps for a maximum of 1 minute.
///
static readonly TimeSpan MaxInterval = new TimeSpan(0, 1, 0);
Timer _timer;
bool _stopped;
DateTime _lastTime;
readonly List _serviceTasks;
#region Initialization
public ServiceTaskScheduler(List serviceTasks)
{
_stopped = true;
_serviceTasks = serviceTasks;
}
#endregion
#region Start
public void Start()
{
_stopped = false;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
_timer = new Timer(OnTimerElapsed);
QueueNextTime(DateTime.Now, true);
}
#endregion
#region Stop
public void Stop()
{
_stopped = true;
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
if(_timer != null)
{
_timer.Dispose();
_timer = null;
}
}
#endregion
#region OnUnobservedTaskException
void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
// This is a last line of defense and is used to handle completely unexpected exceptions.
// The handler runs on the finalizer thread so it’s probably too late to really recover from this.
var ex = e.Exception.Flatten();
// prevent the process from terminating
e.SetObserved();
if(UnhandledError != null)
{
UnhandledError(this, new ErrorEventArgs(ex));
}
}
#endregion
#region OnTimerElapsed
void OnTimerElapsed(object state)
{
var signalTime = DateTime.Now;
StopTimer((Timer)state);
foreach(var serviceTask in _serviceTasks.Where(serviceTask => serviceTask.Schedule.IsDue(_lastTime, signalTime)))
{
if(serviceTask.IsBusy)
{
if(TaskBusy != null)
{
TaskBusy(this, new ServiceTaskEventArgs(serviceTask, signalTime));
}
}
else
{
// Set the LastRunTime before starting the
// task so that the task will calculate the correct
// NextRunTime when QueueNextTime is called below.
//
// Tasks are started asynchronously which means
// that if you put this logic in the Run method
// QueueNextTime will use the current LastRunTime value.
//
serviceTask.Schedule.LastRunTime = signalTime;
Task.Factory.StartNew(taskState => ((ServiceTask)taskState).Run(), serviceTask)
.ContinueWith(task =>
{
var st = (ServiceTask)task.AsyncState;
if(task.Exception != null)
{
var tex = task.Exception.Flatten();
st.Exceptions = tex.InnerExceptions;
}
if(TaskCompleted != null)
{
TaskCompleted(this, new ServiceTaskEventArgs(st, st.Schedule.LastRunTime));
}
});
}
}
if(!_stopped)
{
QueueNextTime(signalTime);
}
}
#endregion
#region QueueNextTime
void QueueNextTime(DateTime thisTime, bool isStarting = false)
{
_lastTime = thisTime;
if(isStarting)
{
foreach(var t in _serviceTasks)
{
t.Schedule.LastRunTime = thisTime;
}
}
var next = _serviceTasks.Min(st => st.Schedule.NextRunTime(thisTime));
var interval = next - thisTime;
//Handles an invalid wait time: the interval property requires a duration > 0.
if(interval > MaxInterval || interval.TotalMilliseconds <= 0)
{
interval = MaxInterval;
}
StartTimer(_timer, interval.TotalMilliseconds);
}
#endregion
#region Start/Stop Timer Methods
static void StopTimer(Timer timer)
{
timer.Change(Timeout.Infinite, 0);
}
static void StartTimer(Timer timer, double interval)
{
timer.Change((long)interval, 0);
}
#endregion
}
}