#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 { /// <summary> /// 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 /// </summary> public class AsyncManager<TOperationId> : IIdentifiableEntity, IDisposable { #region Fields private int id; private IIdentifiableEntity parentEntity; private readonly IServiceLocator serviceLocator; private List<AsyncOperation<TOperationId>> outstandingOperationList = new List<AsyncOperation<TOperationId>>(); private object outstandingOperationListLock = new object(); private ITimer cleaningTimer; private TimeSpan cleanOutstandingOperationsOlderThan; private readonly Func<TOperationId> nextOperationId = null; #endregion #region Construction /// <summary> /// Initializes a new instance of the AsyncManager. /// Uses a default timeout of 1 hour. /// </summary> /// <param name="id">The Id.</param> /// <param name="parentEntity">The parent entity.</param> protected AsyncManager(int id, IIdentifiableEntity parentEntity) : this(id, parentEntity, ServiceContainerFactory.Create()) { } /// <summary> /// Initializes a new instance of the AsyncManager. /// Uses a default timeout of 1 hour. /// </summary> /// <param name="id">The Id.</param> /// <param name="parentEntity">The parent entity.</param> /// <param name="serviceLocator"></param> protected AsyncManager(int id, IIdentifiableEntity parentEntity, IServiceLocator serviceLocator) : this(id, parentEntity, serviceLocator, TimeSpan.FromHours(1)) { } /// <summary> /// 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. /// </summary> /// <param name="id">The Id.</param> /// <param name="parentEntity">The parent entity.</param> /// <param name="serviceLocator"></param> /// <param name="cleanOutstandingOperationsOlderThan">Sets the maximum age an operation can achieve. Minimum age is 1 minute. /// If null no timer is created.</param> protected AsyncManager(int id, IIdentifiableEntity parentEntity, IServiceLocator serviceLocator, TimeSpan? cleanOutstandingOperationsOlderThan) { this.id = id; this.parentEntity = parentEntity; this.serviceLocator = serviceLocator; ITimerFactory timerFactory = serviceLocator.GetServiceOrDefault<ITimerFactory>(() => 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); } } /// <summary> /// 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. /// </summary> /// <param name="id">The Id.</param> /// <param name="parentEntity">The parent entity.</param> /// <param name="serviceLocator"></param> /// <param name="cleanOutstandingOperationsOlderThan">Sets the maximum age an operation can achieve. Minimum age is 1 minute. /// If null no timer is created.</param> /// <param name="nextOperationId"></param> public AsyncManager(int id, IIdentifiableEntity parentEntity, IServiceLocator serviceLocator, TimeSpan? cleanOutstandingOperationsOlderThan, Func<TOperationId> nextOperationId) { this.id = id; this.parentEntity = parentEntity; this.serviceLocator = serviceLocator; this.nextOperationId = nextOperationId; ITimerFactory timerFactory = serviceLocator.GetServiceOrDefault<ITimerFactory>(() => 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); } } /// <summary> /// Finalizer /// </summary> ~AsyncManager() { Dispose(false); } #endregion #region Protected virtual methods /// <summary> /// Method that should be overridden by descendant classes in order to create /// a new operation id to be used by the next operation. /// </summary> /// <returns></returns> protected virtual TOperationId CreateNextOperationId() { return nextOperationId(); } #endregion #region Public Methods /// <summary> /// 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. /// </summary> /// <typeparam name="TResultEventArgs">Type of the EventArgs to the result delegate.</typeparam> /// <param name="owner">The object that is going to be set as sender in the result delegate invocation.</param> /// <param name="resultDelegate">Delegate to be</param> /// <param name="userToken">User token that is returned in the calback invokation.</param> public AsyncOperation<TOperationId, TResultEventArgs> RegisterOperation<TResultEventArgs>(object owner, EventHandler<TResultEventArgs> resultDelegate, object userToken) where TResultEventArgs : EventArgs { return RegisterOperation(owner, resultDelegate, userToken, null); } /// <summary> /// 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. /// </summary> /// <typeparam name="TResultEventArgs">Type of the EventArgs to the result delegate.</typeparam> /// <param name="owner">The object that is going to be set as sender in the result delegate invocation.</param> /// <param name="resultDelegate">Delegate to be</param> /// <param name="userToken">User token that is returned in the calback invokation.</param> /// <param name="data">Application defined data.</param> public AsyncOperation<TOperationId, TResultEventArgs> RegisterOperation<TResultEventArgs>(object owner, EventHandler<TResultEventArgs> resultDelegate, object userToken, object data) where TResultEventArgs : EventArgs { lock (outstandingOperationListLock) { TOperationId newOperationId = CreateNextOperationId(); AsyncOperation<TOperationId, TResultEventArgs> asyncOperation = new AsyncOperation<TOperationId, TResultEventArgs>(owner, newOperationId, userToken, data, resultDelegate, cleanOutstandingOperationsOlderThan); outstandingOperationList.Add(asyncOperation); asyncOperation.OnOperationCompleted += asyncOperation_OnOperationCompleted; return asyncOperation; } } /// <summary> /// Retrieves an outstanding operation by the operation id /// </summary> /// <typeparam name="TResultEventArgs">The type that the operation is expected to be containing, so we know what we expect to get as a result.</typeparam> /// <param name="operationId">The operation Id to search for.</param> /// <param name="asyncOperation">Out parameter that returns the matching operation. Null if it is not found.</param> /// <returns>True if a matching operation is found, otherwise false.</returns> public bool TryGetOperation<TResultEventArgs>(TOperationId operationId, out AsyncOperation<TOperationId, TResultEventArgs> asyncOperation) where TResultEventArgs : EventArgs { asyncOperation = null; lock (outstandingOperationListLock) { foreach (AsyncOperation<TOperationId> op in outstandingOperationList) { if ((op.Id.Equals(operationId))) { asyncOperation = op as AsyncOperation<TOperationId, TResultEventArgs>; if (asyncOperation != null) return true; else return false; } } } return false; } /// <summary> /// Checks whether the requested Operation exists in the list. /// </summary> /// <param name="operationId">The operation Id to search for.</param> /// <returns>True if a matching operation is found, otherwise false.</returns> public bool OperationExists(TOperationId operationId) { lock (outstandingOperationListLock) { foreach (AsyncOperation<TOperationId> op in outstandingOperationList) if ((op.Id.Equals(operationId))) return true; } return false; } /// <summary> /// Returns the EventArgs-type if a matching operation is found, otherwise null. /// </summary> /// <param name="operationId">The operation Id to search for.</param> /// <returns>The EventArgs-type if a matching operation is found, otherwise null.</returns> public Type GetOperationType(TOperationId operationId) { lock (outstandingOperationListLock) { foreach (AsyncOperation<TOperationId> op in outstandingOperationList) if ((op.Id.Equals(operationId))) return op.ResultEventArgsType; } return null; } /// <summary> /// Gets a list of outstanding operations that matches the the specified userToken. Checks and converts the operation to the specified type. /// </summary> /// <typeparam name="TResultEventArgs">Type of the result event args.</typeparam> /// <param name="userToken">User token to match the operation against.</param> /// <param name="asyncOperationList">List of matching operations.</param> [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")] public void GetOperationsByUserToken<TResultEventArgs>(object userToken, out AsyncOperation<TOperationId, TResultEventArgs>[] asyncOperationList) where TResultEventArgs : EventArgs { List<AsyncOperation<TOperationId, TResultEventArgs>> list = new List<AsyncOperation<TOperationId, TResultEventArgs>>(); lock (outstandingOperationListLock) { foreach (AsyncOperation<TOperationId> op in outstandingOperationList) { if (op.UserToken != null) { if ((op.UserToken.Equals(userToken))) { AsyncOperation<TOperationId, TResultEventArgs> asyncOperation = op as AsyncOperation<TOperationId, TResultEventArgs>; if (asyncOperation != null) list.Add(asyncOperation); } } else { if (userToken == null) { AsyncOperation<TOperationId, TResultEventArgs> asyncOperation = op as AsyncOperation<TOperationId, TResultEventArgs>; if (asyncOperation != null) list.Add(asyncOperation); } } } } asyncOperationList = list.ToArray(); } /// <summary> /// Gets an outstanding operation list using the userToken as argument. Does not check the type of the operation. /// </summary> /// <param name="userToken">User token that is specified for the outstanding operation.</param> /// <returns>A list of Async operations.</returns> public AsyncOperation<TOperationId>[] GetOperationsByUserToken(object userToken) { List<AsyncOperation<TOperationId>> list = new List<AsyncOperation<TOperationId>>(); lock (outstandingOperationListLock) { foreach (AsyncOperation<TOperationId> 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(); } /// <summary> /// Gets a list of outstanding operations that matches the the specified data object. Checks and converts the operation to the specified type. /// </summary> /// <typeparam name="TResultEventArgs">Type of the result event args.</typeparam> /// <param name="data">Data object to match the operation against.</param> /// <param name="asyncOperationList">List of matching operations.</param> [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")] public void GetOperationsByData<TResultEventArgs>(object data, out AsyncOperation<TOperationId, TResultEventArgs>[] asyncOperationList) where TResultEventArgs : EventArgs { List<AsyncOperation<TOperationId, TResultEventArgs>> list = new List<AsyncOperation<TOperationId, TResultEventArgs>>(); lock (outstandingOperationListLock) { foreach (AsyncOperation<TOperationId> op in outstandingOperationList) { if (op.Data != null) { if ((op.Data.Equals(data))) { AsyncOperation<TOperationId, TResultEventArgs> asyncOperation = op as AsyncOperation<TOperationId, TResultEventArgs>; if (asyncOperation != null) list.Add(asyncOperation); } } else { if (data == null) { AsyncOperation<TOperationId, TResultEventArgs> asyncOperation = op as AsyncOperation<TOperationId, TResultEventArgs>; if (asyncOperation != null) list.Add(asyncOperation); } } } } asyncOperationList = list.ToArray(); } /// <summary> /// Gets an outstanding operation list using the data object as argument. Does not check the type of the operation. /// </summary> /// <param name="data">User token that is specified for the outstanding operation.</param> /// <returns>A list of Async operations.</returns> public AsyncOperation<TOperationId>[] GetOperationsByData(object data) { List<AsyncOperation<TOperationId>> list = new List<AsyncOperation<TOperationId>>(); lock (outstandingOperationListLock) { foreach (AsyncOperation<TOperationId> 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(); } /// <summary> /// Clear all outstanding operations. /// </summary> public void Clear() { lock (outstandingOperationListLock) { outstandingOperationList.Clear(); } } #endregion #region Private Methods /// <summary> /// Calculates the timespan based on simple rules. /// </summary> /// <returns>The timespan object to be used for the cleaningTimers period and duration</returns> 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; } /// <summary> /// Event handler that is called when an asynchronous operation has completed. It will remove the /// operation from the outstanding operations list. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> void asyncOperation_OnOperationCompleted(object sender, EventArgs e) { AsyncOperation<TOperationId> asyncOperation = sender as AsyncOperation<TOperationId>; if (asyncOperation != null) { lock (outstandingOperationListLock) { if (outstandingOperationList.Contains(asyncOperation)) { outstandingOperationList.Remove(asyncOperation); } } } } /// <summary> /// Timer method that is called to clean the outstanding queue. For now, the requests must not stay /// more than 1h in the outstanding list. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> void CleanOutstandingOperations(object sender, EventArgs e) { AsyncOperation<TOperationId>[] 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<TOperationId> 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 /// <summary> /// Async manager Id (for logging) /// </summary> public int Id { get { return id; } } /// <summary> /// Entity type (for logging) /// </summary> public string EntityType { get { return "AsyncManager"; } } /// <summary> /// This is used by the logger and should never be set by inheriting classes /// </summary> public string FullEntityName { get; set; } /// <summary> /// Entity sub type (for logging) /// </summary> public virtual string EntitySubType { get { return ""; } } /// <summary> /// Parent entity (for logging) /// </summary> public IIdentifiableEntity ParentEntity { get { return parentEntity; } } #endregion #region IDisposable Members /// <summary> /// Dispsose. /// </summary> /// <param name="disposing"></param> protected virtual void Dispose(bool disposing) { if (disposing) { if (cleaningTimer != null) cleaningTimer.Dispose(); } } /// <summary> /// Dispsose. /// </summary> public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion } }