C# class to encapsulate and run cancelable action in the background.
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace DotnetCommon.Utils
{
public class PeriodicAction : IDisposable
{
private const int waitHandleThresholdCount = 258;
private readonly AutoResetEvent _updateAsap = new AutoResetEvent(false);
private readonly AutoResetEvent _intervalChanged = new AutoResetEvent(false);
private readonly Action<CancellationToken> _action;
private CancellationTokenSource _ownCancellationSource;
private CancellationTokenSource _cancellationSource;
private Task _task;
private TimeSpan _interval;
/// <summary>
/// Encapsulate and run cancelable action in the background.
/// </summary>
/// <param name="action">The action to trigger periodically.</param>
/// <param name="cancellationTokenSource">A <see cref="CancellationTokenSource"/> object that will be called if the action in cancelled.</param>
public PeriodicAction(Action<CancellationToken> action, CancellationTokenSource cancellationTokenSource = null)
{
_action = action;
_ownCancellationSource = new CancellationTokenSource();
_cancellationSource = cancellationTokenSource;
_task = Task.Factory.StartNew(Run, _ownCancellationSource.Token);
_interval = TimeSpan.FromMinutes(1.0);
}
public void Dispose()
{
if (_task == null)
{
return;
}
_ownCancellationSource?.Cancel();
try
{
_task.Wait();
}
catch (AggregateException ex)
{
if (ex.InnerExceptions.Count <= 1 && ex.InnerException is TaskCanceledException)
{
return;
}
throw;
}
finally
{
_task = null;
if (_ownCancellationSource != null)
{
_ownCancellationSource.Dispose();
_ownCancellationSource = null;
}
_cancellationSource = null;
_updateAsap.Close();
_updateAsap.Dispose();
_intervalChanged.Close();
_intervalChanged.Dispose();
}
}
/// <summary>
/// The <see cref="TimeSpan"/> interval in which the action will be triggered.
/// </summary>
public TimeSpan Interval
{
get
{
return _interval;
}
set
{
if (_interval == value)
{
return;
}
_interval = value;
_intervalChanged.Set();
}
}
/// <summary>
/// Whether or not the action will trigger.
///
/// <c>true</c> indicates the action will trigger, <c>false</c> indicates
/// the action will not trigger.
/// </summary>
public bool IsEnabled { get; set; }
/// <summary>
/// Whether or not to perform the action asap; without waiting
/// for the specified <see cref="Interval"/> to elapse.
/// </summary>
public void PerformAsap()
{
_updateAsap.Set();
}
private void Run()
{
try
{
var waitHandles = new WaitHandle[3]
{
_ownCancellationSource.Token.WaitHandle,
_intervalChanged,
_updateAsap
};
if (_cancellationSource != null)
{
waitHandles = waitHandles.Concat(
new WaitHandle[1]
{
_cancellationSource.Token.WaitHandle
}).ToArray();
}
int index;
while ((index = WaitHandle.WaitAny(waitHandles, Interval)) == waitHandleThresholdCount ||
waitHandles[index] == _intervalChanged || waitHandles[index] == _updateAsap)
{
if ((index == waitHandleThresholdCount || waitHandles[index] != _intervalChanged) && IsEnabled)
{
_action((_cancellationSource ?? _ownCancellationSource).Token);
}
}
}
catch
{
// absorb exception...
}
}
}
}
// ------
using System;
using System.Diagnostics;
using System.Threading;
using DotnetCommon.Utils;
using Xunit;
namespace DotnetCommon.Tests.Utils
{
public class PeriodicActionTests
{
private static int _periodicActionTriggeredCount;
[Fact]
public void TestPeriodicActionIsTriggeredOnInterval()
{
const int expectedActionTriggeredCount = 4;
_periodicActionTriggeredCount = 0;
using (var cancellationToken = new CancellationTokenSource())
{
cancellationToken.Token.Register(() => { Debug.WriteLine("[{0}] Periodic action cancelled.", DateTime.Now); });
using (var periodicAction = new PeriodicAction(LogToConsole, cancellationToken) { Interval = TimeSpan.FromSeconds(2) })
{
periodicAction.IsEnabled = true;
periodicAction.PerformAsap();
Thread.Sleep(TimeSpan.FromSeconds(8));
}
}
Assert.Equal(expectedActionTriggeredCount, _periodicActionTriggeredCount);
}
[Fact]
public void TestPeriodicActionDoesNotTriggeredIfDisabled()
{
const int expectedActionTriggeredCount = 0;
_periodicActionTriggeredCount = 0;
using (var cancellationToken = new CancellationTokenSource())
{
cancellationToken.Token.Register(() => { Debug.WriteLine("[{0}] Periodic action cancelled.", DateTime.Now); });
using (var periodicAction = new PeriodicAction(LogToConsole, cancellationToken) { Interval = TimeSpan.FromSeconds(2) })
{
periodicAction.IsEnabled = false;
periodicAction.PerformAsap();
Thread.Sleep(TimeSpan.FromSeconds(8));
}
}
Assert.Equal(expectedActionTriggeredCount, _periodicActionTriggeredCount);
}
[Fact]
public void TestPeriodicActionDoesNotTriggeredWhenDisabledToggled()
{
const int expectedActionTriggeredCount = 2;
_periodicActionTriggeredCount = 0;
using (var cancellationToken = new CancellationTokenSource())
{
cancellationToken.Token.Register(() => { Debug.WriteLine("[{0}] Periodic action cancelled.", DateTime.Now); });
using (var periodicAction = new PeriodicAction(LogToConsole, cancellationToken) { Interval = TimeSpan.FromSeconds(2) })
{
periodicAction.IsEnabled = true;
periodicAction.PerformAsap();
Thread.Sleep(TimeSpan.FromSeconds(2));
periodicAction.IsEnabled = false;
Thread.Sleep(TimeSpan.FromSeconds(2));
periodicAction.IsEnabled = true;
Thread.Sleep(TimeSpan.FromSeconds(2));
}
}
Assert.Equal(expectedActionTriggeredCount, _periodicActionTriggeredCount);
}
[Fact]
public void EnsureCancelledActionIsNotTriggered()
{
const int expectedActionTriggeredCount = 1;
_periodicActionTriggeredCount = 0;
using (var cancellationToken = new CancellationTokenSource())
{
cancellationToken.Token.Register(() => { Debug.WriteLine("[{0}] Periodic action cancelled.", DateTime.Now); });
using (var periodicAction = new PeriodicAction(LogToConsole, cancellationToken) { Interval = TimeSpan.FromSeconds(2) })
{
periodicAction.IsEnabled = true;
periodicAction.PerformAsap();
Thread.Sleep(TimeSpan.FromSeconds(2));
periodicAction.IsEnabled = false;
Thread.Sleep(TimeSpan.FromSeconds(2));
periodicAction.IsEnabled = true;
cancellationToken.Cancel();
Thread.Sleep(TimeSpan.FromSeconds(4));
}
}
Assert.Equal(expectedActionTriggeredCount, _periodicActionTriggeredCount);
}
private static void LogToConsole(CancellationToken cancellationToken)
{
if (!cancellationToken.IsCancellationRequested)
{
Interlocked.Increment(ref _periodicActionTriggeredCount);
Debug.WriteLine(
"[{0}] Periodic action triggered {1} time{2}.",
DateTime.Now,
_periodicActionTriggeredCount,
_periodicActionTriggeredCount > 1 ? "s" : "");
}
else
{
cancellationToken.ThrowIfCancellationRequested();
}
}
}
}