#region --------------- Copyright Dresser Wayne Pignone -------------
/*
* $Log: /Wrk/WayneLibraries/Wrk/AsyncManager/AsyncManager.cs $
*
* 12 08-03-19 12:09 Mattias.larsson
* Added GetOperationsByData() and Clear().
*
* 11 08-01-31 12:12 Mattias.larsson
*
* 10 07-05-28 9:40 roger.månsson
* Be more tolerant to null as user token.
*
* 9 07-04-16 12:37 roger.månsson
* Added Data, as an application-defined extra value that can be stored
* with the async operation.
*
* 8 07-03-22 9:53 Mattias.larsson
*
* 7 07-03-13 12:53 roger.månsson
* Fixed the TryGetOperation return value when getting an operation but
* with wrong type.
*
* 6 07-02-15 17:48 roger.månsson
* FxCop updates
*
* 5 07-01-12 16:36 roger.månsson
* Only log when actually cleaning operations from the internal list
*/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Wayne.Lib.Log;
namespace Wayne.Lib.AsyncManager
{
///
/// Async manager is used to handle asynchronous call tracking. It saves data about the
/// asynchronous call so it can be signaled later on. This implementation is based on a operation id type that
/// is defined by the type parameter TOperationId. For example if we are communicating with some equipment and
/// there is a sequence number in the communication, this can be the operation id in the async manager. On the
///
public class AsyncManager : IIdentifiableEntity, IDisposable
{
#region Fields
private int id;
private IIdentifiableEntity parentEntity;
private readonly IServiceLocator serviceLocator;
private List> outstandingOperationList = new List>();
private object outstandingOperationListLock = new object();
private ITimer cleaningTimer;
private TimeSpan cleanOutstandingOperationsOlderThan;
private readonly Func nextOperationId = null;
#endregion
#region Construction
///
/// Initializes a new instance of the AsyncManager.
/// Uses a default timeout of 1 hour.
///
/// The Id.
/// The parent entity.
protected AsyncManager(int id, IIdentifiableEntity parentEntity)
: this(id, parentEntity, ServiceContainerFactory.Create())
{
}
///
/// Initializes a new instance of the AsyncManager.
/// Uses a default timeout of 1 hour.
///
/// The Id.
/// The parent entity.
///
protected AsyncManager(int id, IIdentifiableEntity parentEntity, IServiceLocator serviceLocator)
: this(id, parentEntity, serviceLocator, TimeSpan.FromHours(1))
{
}
///
/// Initializes a new instance of the AsyncManager with a specific cleanOutstandingOperations max age.
/// The timer is triggered after a calculated period between 30 seconds and 10 minutes,
/// if the TimeSpan is not null.
///
/// The Id.
/// The parent entity.
///
/// Sets the maximum age an operation can achieve. Minimum age is 1 minute.
/// If null no timer is created.
protected AsyncManager(int id, IIdentifiableEntity parentEntity, IServiceLocator serviceLocator, TimeSpan? cleanOutstandingOperationsOlderThan)
{
this.id = id;
this.parentEntity = parentEntity;
this.serviceLocator = serviceLocator;
ITimerFactory timerFactory = serviceLocator.GetServiceOrDefault(() => new WayneTimerFactory());
if (cleanOutstandingOperationsOlderThan.HasValue)
{
this.cleanOutstandingOperationsOlderThan = cleanOutstandingOperationsOlderThan.Value;
TimeSpan timerDurationAndPeriod =
CalculatecleaningTimerTriggerLengthFromcleanOutstandingOperationsOlderThan();
cleaningTimer = timerFactory.Create(IdentifiableEntity.NoId, this, "CleaningTimer");
cleaningTimer.OnTimeout += CleanOutstandingOperations;
cleaningTimer.Change(timerDurationAndPeriod, timerDurationAndPeriod);
}
}
///
/// Initializes a new instance of the AsyncManager with a specific cleanOutstandingOperations max age.
/// The timer is triggered after a calculated period between 30 seconds and 10 minutes,
/// if the TimeSpan is not null.
///
/// The Id.
/// The parent entity.
///
/// Sets the maximum age an operation can achieve. Minimum age is 1 minute.
/// If null no timer is created.
///
public AsyncManager(int id, IIdentifiableEntity parentEntity, IServiceLocator serviceLocator, TimeSpan? cleanOutstandingOperationsOlderThan, Func nextOperationId)
{
this.id = id;
this.parentEntity = parentEntity;
this.serviceLocator = serviceLocator;
this.nextOperationId = nextOperationId;
ITimerFactory timerFactory = serviceLocator.GetServiceOrDefault(() => new WayneTimerFactory());
if (cleanOutstandingOperationsOlderThan.HasValue)
{
this.cleanOutstandingOperationsOlderThan = cleanOutstandingOperationsOlderThan.Value;
TimeSpan timerDurationAndPeriod =
CalculatecleaningTimerTriggerLengthFromcleanOutstandingOperationsOlderThan();
cleaningTimer = timerFactory.Create(IdentifiableEntity.NoId, this, "CleaningTimer");
cleaningTimer.OnTimeout += CleanOutstandingOperations;
cleaningTimer.Change(timerDurationAndPeriod, timerDurationAndPeriod);
}
}
///
/// Finalizer
///
~AsyncManager()
{
Dispose(false);
}
#endregion
#region Protected virtual methods
///
/// Method that should be overridden by descendant classes in order to create
/// a new operation id to be used by the next operation.
///
///
protected virtual TOperationId CreateNextOperationId()
{
return nextOperationId();
}
#endregion
#region Public Methods
///
/// Registers an operation with the owner, result delegate and a user token, so it can be found later on. It creates an operation id that can be read from the AsyncOperation that
/// is returned from the function.
///
/// Type of the EventArgs to the result delegate.
/// The object that is going to be set as sender in the result delegate invocation.
/// Delegate to be
/// User token that is returned in the calback invokation.
public AsyncOperation RegisterOperation(object owner, EventHandler resultDelegate, object userToken) where TResultEventArgs : EventArgs
{
return RegisterOperation(owner, resultDelegate, userToken, null);
}
///
/// Registers an operation with the owner, result delegate and a user token, so it can be found later on. It creates an operation id that can be read from the AsyncOperation that
/// is returned from the function. The application can an application-defined object that is to be stored with the operation.
///
/// Type of the EventArgs to the result delegate.
/// The object that is going to be set as sender in the result delegate invocation.
/// Delegate to be
/// User token that is returned in the calback invokation.
/// Application defined data.
public AsyncOperation RegisterOperation(object owner, EventHandler resultDelegate, object userToken, object data) where TResultEventArgs : EventArgs
{
lock (outstandingOperationListLock)
{
TOperationId newOperationId = CreateNextOperationId();
AsyncOperation asyncOperation = new AsyncOperation(owner, newOperationId, userToken, data, resultDelegate, cleanOutstandingOperationsOlderThan);
outstandingOperationList.Add(asyncOperation);
asyncOperation.OnOperationCompleted += asyncOperation_OnOperationCompleted;
return asyncOperation;
}
}
///
/// Retrieves an outstanding operation by the operation id
///
/// The type that the operation is expected to be containing, so we know what we expect to get as a result.
/// The operation Id to search for.
/// Out parameter that returns the matching operation. Null if it is not found.
/// True if a matching operation is found, otherwise false.
public bool TryGetOperation(TOperationId operationId, out AsyncOperation asyncOperation) where TResultEventArgs : EventArgs
{
asyncOperation = null;
lock (outstandingOperationListLock)
{
foreach (AsyncOperation op in outstandingOperationList)
{
if ((op.Id.Equals(operationId)))
{
asyncOperation = op as AsyncOperation;
if (asyncOperation != null)
return true;
else
return false;
}
}
}
return false;
}
///
/// Checks whether the requested Operation exists in the list.
///
/// The operation Id to search for.
/// True if a matching operation is found, otherwise false.
public bool OperationExists(TOperationId operationId)
{
lock (outstandingOperationListLock)
{
foreach (AsyncOperation op in outstandingOperationList)
if ((op.Id.Equals(operationId)))
return true;
}
return false;
}
///
/// Returns the EventArgs-type if a matching operation is found, otherwise null.
///
/// The operation Id to search for.
/// The EventArgs-type if a matching operation is found, otherwise null.
public Type GetOperationType(TOperationId operationId)
{
lock (outstandingOperationListLock)
{
foreach (AsyncOperation op in outstandingOperationList)
if ((op.Id.Equals(operationId)))
return op.ResultEventArgsType;
}
return null;
}
///
/// Gets a list of outstanding operations that matches the the specified userToken. Checks and converts the operation to the specified type.
///
/// Type of the result event args.
/// User token to match the operation against.
/// List of matching operations.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")]
public void GetOperationsByUserToken(object userToken, out AsyncOperation[] asyncOperationList) where TResultEventArgs : EventArgs
{
List> list = new List>();
lock (outstandingOperationListLock)
{
foreach (AsyncOperation op in outstandingOperationList)
{
if (op.UserToken != null)
{
if ((op.UserToken.Equals(userToken)))
{
AsyncOperation asyncOperation = op as AsyncOperation;
if (asyncOperation != null)
list.Add(asyncOperation);
}
}
else
{
if (userToken == null)
{
AsyncOperation asyncOperation = op as AsyncOperation;
if (asyncOperation != null)
list.Add(asyncOperation);
}
}
}
}
asyncOperationList = list.ToArray();
}
///
/// Gets an outstanding operation list using the userToken as argument. Does not check the type of the operation.
///
/// User token that is specified for the outstanding operation.
/// A list of Async operations.
public AsyncOperation[] GetOperationsByUserToken(object userToken)
{
List> list = new List>();
lock (outstandingOperationListLock)
{
foreach (AsyncOperation op in outstandingOperationList)
{
if (op.UserToken != null)
{
if ((op.UserToken.Equals(userToken)))
{
list.Add(op);
}
}
else
{
if (userToken == null) //If the requesting user token also is null, it is a valid match!
list.Add(op);
}
}
}
return list.ToArray();
}
///
/// Gets a list of outstanding operations that matches the the specified data object. Checks and converts the operation to the specified type.
///
/// Type of the result event args.
/// Data object to match the operation against.
/// List of matching operations.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")]
public void GetOperationsByData(object data, out AsyncOperation[] asyncOperationList) where TResultEventArgs : EventArgs
{
List> list = new List>();
lock (outstandingOperationListLock)
{
foreach (AsyncOperation op in outstandingOperationList)
{
if (op.Data != null)
{
if ((op.Data.Equals(data)))
{
AsyncOperation asyncOperation = op as AsyncOperation;
if (asyncOperation != null)
list.Add(asyncOperation);
}
}
else
{
if (data == null)
{
AsyncOperation asyncOperation = op as AsyncOperation;
if (asyncOperation != null)
list.Add(asyncOperation);
}
}
}
}
asyncOperationList = list.ToArray();
}
///
/// Gets an outstanding operation list using the data object as argument. Does not check the type of the operation.
///
/// User token that is specified for the outstanding operation.
/// A list of Async operations.
public AsyncOperation[] GetOperationsByData(object data)
{
List> list = new List>();
lock (outstandingOperationListLock)
{
foreach (AsyncOperation op in outstandingOperationList)
{
if (op.Data != null)
{
if ((op.Data.Equals(data)))
{
list.Add(op);
}
}
else
{
if (data == null) //If the requesting user token also is null, it is a valid match!
list.Add(op);
}
}
}
return list.ToArray();
}
///
/// Clear all outstanding operations.
///
public void Clear()
{
lock (outstandingOperationListLock)
{
outstandingOperationList.Clear();
}
}
#endregion
#region Private Methods
///
/// Calculates the timespan based on simple rules.
///
/// The timespan object to be used for the cleaningTimers period and duration
private TimeSpan CalculatecleaningTimerTriggerLengthFromcleanOutstandingOperationsOlderThan()
{
TimeSpan result;
if (cleanOutstandingOperationsOlderThan >= TimeSpan.FromHours(1))
{
result = TimeSpan.FromMinutes(10);
}
else if (cleanOutstandingOperationsOlderThan <= TimeSpan.FromMinutes(1))
{
result = TimeSpan.FromSeconds(30);
//Will never accept less values than 1 minute as oldest time an operation might be.
cleanOutstandingOperationsOlderThan = TimeSpan.FromMinutes(1);
}
else
{
//The formula is:
// x = y - b / m
// m = (y2 - y1) / (x2 - x1) => (3600 * 10^3 - 60 * 10^3) / (600 * 10 ^3 - 30 * 10^3) => (354/57) ~= 6,2105...
// b = y1 - m * x1 => 60 * 10^3 - (354/57) * 30 * 10^3 ~= -126315,789476...
result = TimeSpan.FromMilliseconds((cleanOutstandingOperationsOlderThan.TotalMilliseconds + 126315.78947368421052631578947368) / (354.0 / 57.0));
}
return result;
}
///
/// Event handler that is called when an asynchronous operation has completed. It will remove the
/// operation from the outstanding operations list.
///
///
///
void asyncOperation_OnOperationCompleted(object sender, EventArgs e)
{
AsyncOperation asyncOperation = sender as AsyncOperation;
if (asyncOperation != null)
{
lock (outstandingOperationListLock)
{
if (outstandingOperationList.Contains(asyncOperation))
{
outstandingOperationList.Remove(asyncOperation);
}
}
}
}
///
/// Timer method that is called to clean the outstanding queue. For now, the requests must not stay
/// more than 1h in the outstanding list.
///
///
///
void CleanOutstandingOperations(object sender, EventArgs e)
{
AsyncOperation[] abandonedOperations;
lock (outstandingOperationListLock)
{
abandonedOperations = outstandingOperationList.Where(x => x.Timeout.IsTimedOut).ToArray();
}
using (DebugLogger debugLogger = new DebugLogger(this))
{
if (abandonedOperations.Length > 0)
{
//Remove the items that should be removed.
if (debugLogger.IsActive())
{
debugLogger.Add("OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO");
debugLogger.Add("Cleaning the outstanding operations.");
debugLogger.Add("---------------------------------------------------");
}
foreach (AsyncOperation abandonedOperation in abandonedOperations)
{
if (debugLogger.IsActive())
{
debugLogger.Add("Removing operation: ");
debugLogger.Add(abandonedOperation.ToString());
}
try
{
abandonedOperation.Abandoned();
}
catch (Exception exception)
{
//Logger.AddExceptionLogEntry(new ExceptionLogEntry(this, ErrorLogSeverity.Recoverable, "Exception when handling abandoned async operation " + abandonedOperation, exception));
}
}
if (debugLogger.IsActive())
debugLogger.Add("OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO");
}
}
//Remove the found operations, if not removed by abandoned- logic.
lock (outstandingOperationListLock)
{
abandonedOperations.ForEach(x => outstandingOperationList.Remove(x));
}
}
#endregion
#region IIdentifiableEntity Members
///
/// Async manager Id (for logging)
///
public int Id
{
get { return id; }
}
///
/// Entity type (for logging)
///
public string EntityType
{
get { return "AsyncManager"; }
}
///
/// This is used by the logger and should never be set by inheriting classes
///
public string FullEntityName { get; set; }
///
/// Entity sub type (for logging)
///
public virtual string EntitySubType
{
get { return ""; }
}
///
/// Parent entity (for logging)
///
public IIdentifiableEntity ParentEntity
{
get { return parentEntity; }
}
#endregion
#region IDisposable Members
///
/// Dispsose.
///
///
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (cleaningTimer != null)
cleaningTimer.Dispose();
}
}
///
/// Dispsose.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}