I'm currently working on a WPF application, making calls to various web APIs. Thus, I'm looking after a special observable tasks queue:
- Progress value must be available
- Tasks (API calls)/seconds must be limited
- Failed tasks must be restarted (API calls may unexpectedly raise exceptions)
Since the API calls are not very CPU intensive (mostly waiting for web responses), Based on Servy's answer, I kept it simple by using async/await rather than using threads, but I may be wrong.
I came up with the following class, that seems to work well with WPF. But could you tell me what you think about this simple implementation? Any advice about improvements or errors I might have done is welcome!
public class ObservableTaskQueue : INotifyPropertyChanged
{
//========================================================================================================
// Events
//========================================================================================================
/// <summary>
/// Occurs when the first task is enqueued.
/// </summary>
public event Action StartWorking;
/// <summary>
/// Occurs when the last enqueued task is done.
/// </summary>
public event Action StopWorking;
//========================================================================================================
// Fields
//========================================================================================================
private SemaphoreSlim semaphore;
private Stopwatch timer = new Stopwatch();
private int maxTasksCount = 0;
private int queuedTaskCount = 0;
//========================================================================================================
// Properties
//========================================================================================================
/// <summary>
/// Get or set the minimum duration between two task starts (in milliseconds).
/// </summary>
public int MinimumTaskDuration { get; set; }
/// <summary>
/// Get or set whether a task needs to be restarted after raising an exception.
/// </summary>
public bool RestartTaskOnError { get; set; }
/// <summary>
/// Get the task queue's progress (0-100).
/// </summary>
public int Progress {
get
{
return (maxTasksCount == 0) ? 0 : (maxTasksCount - queuedTaskCount) * 100 / maxTasksCount;
}
}
//========================================================================================================
// Constructors
//========================================================================================================
public TaskQueue()
{
semaphore = new SemaphoreSlim(1);
}
//========================================================================================================
// Methods
//========================================================================================================
public async void Enqueue(Func<Task> taskCallback)
{
// Raise event when the queue gets busy.
if (Interlocked.Increment(ref queuedTaskCount) == 1)
{
Interlocked.Exchange(ref maxTasksCount, 0);
if (StartWorking != null)
{
StartWorking();
}
}
// Update queue's progress.
Interlocked.Increment(ref maxTasksCount);
NotifyPropertyChanged("Progress");
// Only one task can be running at one time.
await semaphore.WaitAsync();
// If a maximum task rate is set, wait a bit if the previous task finished too fast.
if (MinimumTaskDuration > 0 && timer.IsRunning)
{
timer.Stop();
if (timer.ElapsedMilliseconds < MinimumTaskDuration)
{
Console.WriteLine(" Waiting {0}ms", MinimumTaskDuration - (int)timer.ElapsedMilliseconds);
await Task.Delay(MinimumTaskDuration - (int)timer.ElapsedMilliseconds + 1);
}
}
// Start measuring task's duration and run the task.
try
{
timer.Restart();
await taskCallback();
}
// Restart failed tasks on error.
catch
{
if (RestartTaskOnError)
{
Interlocked.Decrement(ref maxTasksCount); // don't notify progress yet
Console.WriteLine("Error, requeue the same task");
Enqueue(taskCallback);
}
}
finally
{
Console.WriteLine(" Ended in {0}ms", timer.ElapsedMilliseconds);
// Raise event when the queue gets idle.
if (Interlocked.Decrement(ref queuedTaskCount) == 0)
{
timer.Stop();
if (StopWorking != null)
{
StopWorking();
}
}
NotifyPropertyChanged("Progress");
semaphore.Release();
}
}
//========================================================================================================
// INotifyPropertyChanged implementation
//========================================================================================================
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Usage :
public MainWindow()
{
InitializeComponent();
TaskQueue queue = new TaskQueue();
queue.MinimumTaskDuration = 300;
queue.RestartTaskOnError = true;
queue.StartWorking += delegate() { Console.WriteLine("START WORKING"); };
queue.StopWorking += delegate() { Console.WriteLine("STOP WORKING"); };
Console.WriteLine("Enqueue (Task-1)");
Queue.Enqueue(Task1);
Console.WriteLine("Enqueue (Task-2)");
Queue.Enqueue(Task2);
Console.WriteLine("Enqueue (Task-3)");
Queue.Enqueue(Task3);
}
bool restart = true;
private async Task Task1()
{
Console.WriteLine("(Task-1) started");
await Task.Delay(50);
Console.WriteLine(" (Task-1) completed");
}
private async Task Task2()
{
Console.WriteLine("(Task-2) started");
await Task.Delay(100);
if (restart) // fails on the first try!
{
restart = false;
throw new Exception();
}
Console.WriteLine(" (Task-2) completed");
}
private async Task Task3()
{
Console.WriteLine("(Task-3) started");
await Task.Delay(85);
Console.WriteLine(" (Task-3) completed");
}
Result:
Enqueue (Task-1) [START WORKING] (Task-1) started Enqueue (Task-2) Enqueue (Task-3) (Task-1) completed Ended in 60ms Waiting 239ms (Task-2) started A first chance exception of type 'System.Exception' occured in OkSEO SearchConsole.exe A first chance exception of type 'System.Exception' occured in mscorlib.dll Error occured, requeue the same task Ended in 133ms Waiting 167ms (Task-3) started (Task-3) completed Ended in 86ms Waiting 213ms (Task-2) started (Task-2) completed Ended in 114ms [STOP WORKING]
Known bugs/things to do:
- The class is lacking a generic
Enqueue<T>
method. - Task delay is not respected between two separate queue processings (if a new task is added immediately after the triggering of
StopWorking
event). maxTasksCount
andqueuedTaskCount
might be public observable properties.