#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 } }