AsyncManager.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. #region --------------- Copyright Dresser Wayne Pignone -------------
  2. /*
  3. * $Log: /Wrk/WayneLibraries/Wrk/AsyncManager/AsyncManager.cs $
  4. *
  5. * 12 08-03-19 12:09 Mattias.larsson
  6. * Added GetOperationsByData() and Clear().
  7. *
  8. * 11 08-01-31 12:12 Mattias.larsson
  9. *
  10. * 10 07-05-28 9:40 roger.månsson
  11. * Be more tolerant to null as user token.
  12. *
  13. * 9 07-04-16 12:37 roger.månsson
  14. * Added Data, as an application-defined extra value that can be stored
  15. * with the async operation.
  16. *
  17. * 8 07-03-22 9:53 Mattias.larsson
  18. *
  19. * 7 07-03-13 12:53 roger.månsson
  20. * Fixed the TryGetOperation return value when getting an operation but
  21. * with wrong type.
  22. *
  23. * 6 07-02-15 17:48 roger.månsson
  24. * FxCop updates
  25. *
  26. * 5 07-01-12 16:36 roger.månsson
  27. * Only log when actually cleaning operations from the internal list
  28. */
  29. #endregion
  30. using System;
  31. using System.Collections.Generic;
  32. using Wayne.Lib.Log;
  33. namespace Wayne.Lib.AsyncManager
  34. {
  35. /// <summary>
  36. /// Async manager is used to handle asynchronous call tracking. It saves data about the
  37. /// asynchronous call so it can be signaled later on. This implementation is based on a operation id type that
  38. /// is defined by the type parameter TOperationId. For example if we are communicating with some equipment and
  39. /// there is a sequence number in the communication, this can be the operation id in the async manager. On the
  40. /// </summary>
  41. public abstract class AsyncManager<TOperationId> : IIdentifiableEntity, IDisposable
  42. {
  43. #region Fields
  44. private int id;
  45. private IIdentifiableEntity parentEntity;
  46. private readonly IServiceLocator serviceLocator;
  47. private List<AsyncOperation<TOperationId>> outstandingOperationList = new List<AsyncOperation<TOperationId>>();
  48. private object outstandingOperationListLock = new object();
  49. private ITimer cleaningTimer;
  50. private TimeSpan cleanOutstandingOperationsOlderThan;
  51. private readonly ISystemTime systemTime;
  52. /// <summary>
  53. ///
  54. /// </summary>
  55. public event EventHandler<UserTokenEventArgs> OnCanceledOutstandingOperations;
  56. #endregion
  57. #region Construction
  58. /// <summary>
  59. /// Initializes a new instance of the AsyncManager.
  60. /// Uses a default timeout of 1 hour.
  61. /// </summary>
  62. /// <param name="id">The Id.</param>
  63. /// <param name="parentEntity">The parent entity.</param>
  64. protected AsyncManager(int id, IIdentifiableEntity parentEntity)
  65. : this(id, parentEntity, ServiceContainerFactory.Create())
  66. {
  67. }
  68. /// <summary>
  69. /// Initializes a new instance of the AsyncManager.
  70. /// Uses a default timeout of 1 hour.
  71. /// </summary>
  72. /// <param name="id">The Id.</param>
  73. /// <param name="parentEntity">The parent entity.</param>
  74. /// <param name="serviceLocator"></param>
  75. protected AsyncManager(int id, IIdentifiableEntity parentEntity, IServiceLocator serviceLocator)
  76. : this(id, parentEntity, serviceLocator, TimeSpan.FromHours(1))
  77. {
  78. }
  79. /// <summary>
  80. /// Initializes a new instance of the AsyncManager with a specific cleanOutstandingOperations max age.
  81. /// The timer is triggered after a calculated period between 30 seconds and 10 minutes,
  82. /// if the TimeSpan is not null.
  83. /// </summary>
  84. /// <param name="id">The Id.</param>
  85. /// <param name="parentEntity">The parent entity.</param>
  86. /// <param name="serviceLocator"></param>
  87. /// <param name="cleanOutstandingOperationsOlderThan">Sets the maximum age an operation can achieve. Minimum age is 1 minute.
  88. /// If null no timer is created.</param>
  89. protected AsyncManager(int id, IIdentifiableEntity parentEntity, IServiceLocator serviceLocator, TimeSpan? cleanOutstandingOperationsOlderThan)
  90. {
  91. this.id = id;
  92. this.parentEntity = parentEntity;
  93. this.serviceLocator = serviceLocator;
  94. systemTime = serviceLocator.GetServiceOrDefault<ISystemTime>(() => new WayneSystemTime());
  95. ITimerFactory timerFactory = serviceLocator.GetServiceOrDefault<ITimerFactory>(() => new WayneTimerFactory());
  96. if (cleanOutstandingOperationsOlderThan.HasValue)
  97. {
  98. this.cleanOutstandingOperationsOlderThan = cleanOutstandingOperationsOlderThan.Value;
  99. TimeSpan timerDurationAndPeriod =
  100. CalculatecleaningTimerTriggerLengthFromcleanOutstandingOperationsOlderThan();
  101. cleaningTimer = timerFactory.Create(IdentifiableEntity.NoId, this, "CleaningTimer");
  102. cleaningTimer.OnTimeout += CleanOutstandingOperations;
  103. cleaningTimer.Change(timerDurationAndPeriod, timerDurationAndPeriod);
  104. }
  105. }
  106. /// <summary>
  107. /// Finalizer
  108. /// </summary>
  109. ~AsyncManager()
  110. {
  111. Dispose(false);
  112. }
  113. #endregion
  114. #region Protected virtual methods
  115. /// <summary>
  116. /// Method that should be overridden by descendant classes in order to create
  117. /// a new operation id to be used by the next operation.
  118. /// </summary>
  119. /// <returns></returns>
  120. protected abstract TOperationId CreateNextOperationId();
  121. #endregion
  122. #region Public Methods
  123. /// <summary>
  124. /// 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
  125. /// is returned from the function.
  126. /// </summary>
  127. /// <typeparam name="TResultEventArgs">Type of the EventArgs to the result delegate.</typeparam>
  128. /// <param name="owner">The object that is going to be set as sender in the result delegate invocation.</param>
  129. /// <param name="resultDelegate">Delegate to be</param>
  130. /// <param name="userToken">User token that is returned in the calback invokation.</param>
  131. public AsyncOperation<TOperationId, TResultEventArgs> RegisterOperation<TResultEventArgs>(object owner, EventHandler<TResultEventArgs> resultDelegate, object userToken) where TResultEventArgs : EventArgs
  132. {
  133. return RegisterOperation(owner, resultDelegate, userToken, null);
  134. }
  135. /// <summary>
  136. /// 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
  137. /// is returned from the function. The application can an application-defined object that is to be stored with the operation.
  138. /// </summary>
  139. /// <typeparam name="TResultEventArgs">Type of the EventArgs to the result delegate.</typeparam>
  140. /// <param name="owner">The object that is going to be set as sender in the result delegate invocation.</param>
  141. /// <param name="resultDelegate">Delegate to be</param>
  142. /// <param name="userToken">User token that is returned in the calback invokation.</param>
  143. /// <param name="data">Application defined data.</param>
  144. public AsyncOperation<TOperationId, TResultEventArgs> RegisterOperation<TResultEventArgs>(object owner, EventHandler<TResultEventArgs> resultDelegate, object userToken, object data) where TResultEventArgs : EventArgs
  145. {
  146. lock (outstandingOperationListLock)
  147. {
  148. TOperationId newOperationId = CreateNextOperationId();
  149. AsyncOperation<TOperationId, TResultEventArgs> asyncOperation = new AsyncOperation<TOperationId, TResultEventArgs>(owner, newOperationId, userToken, data, resultDelegate, serviceLocator);
  150. outstandingOperationList.Add(asyncOperation);
  151. asyncOperation.OnOperationCompleted += new EventHandler(asyncOperation_OnOperationCompleted);
  152. return asyncOperation;
  153. }
  154. }
  155. /// <summary>
  156. /// Retrieves an outstanding operation by the operation id
  157. /// </summary>
  158. /// <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>
  159. /// <param name="operationId">The operation Id to search for.</param>
  160. /// <param name="asyncOperation">Out parameter that returns the matching operation. Null if it is not found.</param>
  161. /// <returns>True if a matching operation is found, otherwise false.</returns>
  162. public bool TryGetOperation<TResultEventArgs>(TOperationId operationId, out AsyncOperation<TOperationId, TResultEventArgs> asyncOperation) where TResultEventArgs : EventArgs
  163. {
  164. asyncOperation = null;
  165. lock (outstandingOperationListLock)
  166. {
  167. foreach (AsyncOperation<TOperationId> op in outstandingOperationList)
  168. {
  169. if ((op.Id.Equals(operationId)))
  170. {
  171. asyncOperation = op as AsyncOperation<TOperationId, TResultEventArgs>;
  172. if (asyncOperation != null)
  173. return true;
  174. else
  175. return false;
  176. }
  177. }
  178. }
  179. return false;
  180. }
  181. /// <summary>
  182. /// Checks whether the requested Operation exists in the list.
  183. /// </summary>
  184. /// <param name="operationId">The operation Id to search for.</param>
  185. /// <returns>True if a matching operation is found, otherwise false.</returns>
  186. public bool OperationExists(TOperationId operationId)
  187. {
  188. lock (outstandingOperationListLock)
  189. {
  190. foreach (AsyncOperation<TOperationId> op in outstandingOperationList)
  191. if ((op.Id.Equals(operationId)))
  192. return true;
  193. }
  194. return false;
  195. }
  196. /// <summary>
  197. /// Returns the EventArgs-type if a matching operation is found, otherwise null.
  198. /// </summary>
  199. /// <param name="operationId">The operation Id to search for.</param>
  200. /// <returns>The EventArgs-type if a matching operation is found, otherwise null.</returns>
  201. public Type GetOperationType(TOperationId operationId)
  202. {
  203. lock (outstandingOperationListLock)
  204. {
  205. foreach (AsyncOperation<TOperationId> op in outstandingOperationList)
  206. if ((op.Id.Equals(operationId)))
  207. return op.ResultEventArgsType;
  208. }
  209. return null;
  210. }
  211. /// <summary>
  212. /// Gets a list of outstanding operations that matches the the specified userToken. Checks and converts the operation to the specified type.
  213. /// </summary>
  214. /// <typeparam name="TResultEventArgs">Type of the result event args.</typeparam>
  215. /// <param name="userToken">User token to match the operation against.</param>
  216. /// <param name="asyncOperationList">List of matching operations.</param>
  217. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")]
  218. public void GetOperationsByUserToken<TResultEventArgs>(object userToken, out AsyncOperation<TOperationId, TResultEventArgs>[] asyncOperationList) where TResultEventArgs : EventArgs
  219. {
  220. List<AsyncOperation<TOperationId, TResultEventArgs>> list = new List<AsyncOperation<TOperationId, TResultEventArgs>>();
  221. lock (outstandingOperationListLock)
  222. {
  223. foreach (AsyncOperation<TOperationId> op in outstandingOperationList)
  224. {
  225. if (op.UserToken != null)
  226. {
  227. if ((op.UserToken.Equals(userToken)))
  228. {
  229. AsyncOperation<TOperationId, TResultEventArgs> asyncOperation = op as AsyncOperation<TOperationId, TResultEventArgs>;
  230. if (asyncOperation != null)
  231. list.Add(asyncOperation);
  232. }
  233. }
  234. else
  235. {
  236. if (userToken == null)
  237. {
  238. AsyncOperation<TOperationId, TResultEventArgs> asyncOperation = op as AsyncOperation<TOperationId, TResultEventArgs>;
  239. if (asyncOperation != null)
  240. list.Add(asyncOperation);
  241. }
  242. }
  243. }
  244. }
  245. asyncOperationList = list.ToArray();
  246. }
  247. /// <summary>
  248. /// Gets an outstanding operation list using the userToken as argument. Does not check the type of the operation.
  249. /// </summary>
  250. /// <param name="userToken">User token that is specified for the outstanding operation.</param>
  251. /// <returns>A list of Async operations.</returns>
  252. public AsyncOperation<TOperationId>[] GetOperationsByUserToken(object userToken)
  253. {
  254. List<AsyncOperation<TOperationId>> list = new List<AsyncOperation<TOperationId>>();
  255. lock (outstandingOperationListLock)
  256. {
  257. foreach (AsyncOperation<TOperationId> op in outstandingOperationList)
  258. {
  259. if (op.UserToken != null)
  260. {
  261. if ((op.UserToken.Equals(userToken)))
  262. {
  263. list.Add(op);
  264. }
  265. }
  266. else
  267. {
  268. if (userToken == null) //If the requesting user token also is null, it is a valid match!
  269. list.Add(op);
  270. }
  271. }
  272. }
  273. return list.ToArray();
  274. }
  275. /// <summary>
  276. /// Gets a list of outstanding operations that matches the the specified data object. Checks and converts the operation to the specified type.
  277. /// </summary>
  278. /// <typeparam name="TResultEventArgs">Type of the result event args.</typeparam>
  279. /// <param name="data">Data object to match the operation against.</param>
  280. /// <param name="asyncOperationList">List of matching operations.</param>
  281. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")]
  282. public void GetOperationsByData<TResultEventArgs>(object data, out AsyncOperation<TOperationId, TResultEventArgs>[] asyncOperationList) where TResultEventArgs : EventArgs
  283. {
  284. List<AsyncOperation<TOperationId, TResultEventArgs>> list = new List<AsyncOperation<TOperationId, TResultEventArgs>>();
  285. lock (outstandingOperationListLock)
  286. {
  287. foreach (AsyncOperation<TOperationId> op in outstandingOperationList)
  288. {
  289. if (op.Data != null)
  290. {
  291. if ((op.Data.Equals(data)))
  292. {
  293. AsyncOperation<TOperationId, TResultEventArgs> asyncOperation = op as AsyncOperation<TOperationId, TResultEventArgs>;
  294. if (asyncOperation != null)
  295. list.Add(asyncOperation);
  296. }
  297. }
  298. else
  299. {
  300. if (data == null)
  301. {
  302. AsyncOperation<TOperationId, TResultEventArgs> asyncOperation = op as AsyncOperation<TOperationId, TResultEventArgs>;
  303. if (asyncOperation != null)
  304. list.Add(asyncOperation);
  305. }
  306. }
  307. }
  308. }
  309. asyncOperationList = list.ToArray();
  310. }
  311. /// <summary>
  312. /// Gets an outstanding operation list using the data object as argument. Does not check the type of the operation.
  313. /// </summary>
  314. /// <param name="data">User token that is specified for the outstanding operation.</param>
  315. /// <returns>A list of Async operations.</returns>
  316. public AsyncOperation<TOperationId>[] GetOperationsByData(object data)
  317. {
  318. List<AsyncOperation<TOperationId>> list = new List<AsyncOperation<TOperationId>>();
  319. lock (outstandingOperationListLock)
  320. {
  321. foreach (AsyncOperation<TOperationId> op in outstandingOperationList)
  322. {
  323. if (op.Data != null)
  324. {
  325. if ((op.Data.Equals(data)))
  326. {
  327. list.Add(op);
  328. }
  329. }
  330. else
  331. {
  332. if (data == null) //If the requesting user token also is null, it is a valid match!
  333. list.Add(op);
  334. }
  335. }
  336. }
  337. return list.ToArray();
  338. }
  339. /// <summary>
  340. /// Clear all outstanding operations.
  341. /// </summary>
  342. public void Clear()
  343. {
  344. lock (outstandingOperationListLock)
  345. {
  346. outstandingOperationList.Clear();
  347. }
  348. }
  349. #endregion
  350. #region Private Methods
  351. /// <summary>
  352. /// Calculates the timespan based on simple rules.
  353. /// </summary>
  354. /// <returns>The timespan object to be used for the cleaningTimers period and duration</returns>
  355. private TimeSpan CalculatecleaningTimerTriggerLengthFromcleanOutstandingOperationsOlderThan()
  356. {
  357. TimeSpan result;
  358. if(cleanOutstandingOperationsOlderThan >= TimeSpan.FromHours(1))
  359. {
  360. result = TimeSpan.FromMinutes(10);
  361. }
  362. else if (cleanOutstandingOperationsOlderThan <= TimeSpan.FromMinutes(1))
  363. {
  364. result = TimeSpan.FromSeconds(30);
  365. //Will never accept less values than 1 minute as oldest time an operation might be.
  366. cleanOutstandingOperationsOlderThan = TimeSpan.FromMinutes(1);
  367. }else
  368. {
  369. //The formula is:
  370. // x = y - b / m
  371. // m = (y2 - y1) / (x2 - x1) => (3600 * 10^3 - 60 * 10^3) / (600 * 10 ^3 - 30 * 10^3) => (354/57) ~= 6,2105...
  372. // b = y1 - m * x1 => 60 * 10^3 - (354/57) * 30 * 10^3 ~= -126315,789476...
  373. result = TimeSpan.FromMilliseconds( (cleanOutstandingOperationsOlderThan.TotalMilliseconds + 126315.78947368421052631578947368 )/(354.0/57.0));
  374. }
  375. return result;
  376. }
  377. /// <summary>
  378. /// Event handler that is called when an asynchronous operation has completed. It will remove the
  379. /// operation from the outstanding operations list.
  380. /// </summary>
  381. /// <param name="sender"></param>
  382. /// <param name="e"></param>
  383. void asyncOperation_OnOperationCompleted(object sender, EventArgs e)
  384. {
  385. AsyncOperation<TOperationId> asyncOperation = sender as AsyncOperation<TOperationId>;
  386. if (asyncOperation != null)
  387. {
  388. lock (outstandingOperationListLock)
  389. {
  390. if (outstandingOperationList.Contains(asyncOperation))
  391. {
  392. outstandingOperationList.Remove(asyncOperation);
  393. }
  394. }
  395. }
  396. }
  397. /// <summary>
  398. /// Timer method that is called to clean the outstanding queue. For now, the requests must not stay
  399. /// more than 1h in the outstanding list.
  400. /// </summary>
  401. /// <param name="sender"></param>
  402. /// <param name="e"></param>
  403. void CleanOutstandingOperations(object sender, EventArgs e)
  404. {
  405. List<AsyncOperation<TOperationId>> operationRemovedList = new List<AsyncOperation<TOperationId>>();
  406. lock (outstandingOperationListLock)
  407. {
  408. //Calculate the oldest allowed operation.
  409. DateTime oldestAllowedOperation = systemTime.Now.Subtract(cleanOutstandingOperationsOlderThan);
  410. //Check all operations in the list.
  411. foreach (AsyncOperation<TOperationId> asyncOperation in outstandingOperationList)
  412. {
  413. if (asyncOperation.CreationDateTime < oldestAllowedOperation)
  414. {
  415. operationRemovedList.Add(asyncOperation);
  416. }
  417. }
  418. //Remove the found operations;)
  419. foreach (AsyncOperation<TOperationId> asyncOperationToRemove in operationRemovedList)
  420. {
  421. outstandingOperationList.Remove(asyncOperationToRemove);
  422. }
  423. }
  424. using (DebugLogger debugLogger = new DebugLogger(this))
  425. {
  426. if (operationRemovedList.Count > 0)
  427. {
  428. //Remove the items that should be removed.
  429. if (debugLogger.IsActive())
  430. {
  431. debugLogger.Add("OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO");
  432. debugLogger.Add("Cleaning the outstanding operations.");
  433. debugLogger.Add("---------------------------------------------------");
  434. }
  435. foreach (AsyncOperation<TOperationId> asyncOperationRemoved in operationRemovedList)
  436. {
  437. if (debugLogger.IsActive())
  438. {
  439. debugLogger.Add("Removing operation: ");
  440. debugLogger.Add(asyncOperationRemoved.ToString());
  441. }
  442. asyncOperationRemoved.Cancel();
  443. var disposable = asyncOperationRemoved.Data as IDisposable;
  444. if(disposable != null)
  445. {
  446. try
  447. {
  448. disposable.Dispose();
  449. debugLogger.Add("Cancelled Disposable AsyncOperation disposed.", DebugLogLevel.Detailed);
  450. }
  451. catch (Exception disposedException)
  452. {
  453. if(debugLogger.IsActive())
  454. {
  455. debugLogger.Add("Exception thrown while disposing a Cancelled Disposable AsyncOperation.", DebugLogLevel.Normal);
  456. debugLogger.Add(disposedException.ToString(), DebugLogLevel.Normal);
  457. }
  458. }
  459. asyncOperationRemoved.Data = null;
  460. }
  461. if (OnCanceledOutstandingOperations != null)
  462. OnCanceledOutstandingOperations(this, new UserTokenEventArgs(asyncOperationRemoved.UserToken));
  463. }
  464. if (debugLogger.IsActive())
  465. debugLogger.Add("OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO");
  466. }
  467. }
  468. }
  469. #endregion
  470. #region IIdentifiableEntity Members
  471. /// <summary>
  472. /// Async manager Id (for logging)
  473. /// </summary>
  474. public int Id
  475. {
  476. get { return id; }
  477. }
  478. /// <summary>
  479. /// Entity type (for logging)
  480. /// </summary>
  481. public string EntityType
  482. {
  483. get { return "AsyncManager"; }
  484. }
  485. /// <summary>
  486. /// This is used by the logger and should never be set by inheriting classes
  487. /// </summary>
  488. public string FullEntityName { get; set; }
  489. /// <summary>
  490. /// Entity sub type (for logging)
  491. /// </summary>
  492. public abstract string EntitySubType { get;}
  493. /// <summary>
  494. /// Parent entity (for logging)
  495. /// </summary>
  496. public IIdentifiableEntity ParentEntity
  497. {
  498. get { return parentEntity; }
  499. }
  500. #endregion
  501. #region IDisposable Members
  502. /// <summary>
  503. /// Dispsose.
  504. /// </summary>
  505. /// <param name="disposing"></param>
  506. protected virtual void Dispose(bool disposing)
  507. {
  508. if (disposing)
  509. {
  510. if (cleaningTimer != null)
  511. cleaningTimer.Dispose();
  512. }
  513. }
  514. /// <summary>
  515. /// Dispsose.
  516. /// </summary>
  517. public void Dispose()
  518. {
  519. Dispose(true);
  520. GC.SuppressFinalize(this);
  521. }
  522. #endregion
  523. }
  524. }