PsQueue.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. using System.Collections.Concurrent;
  2. using qdr.fnd.core.pqueue.Configuration;
  3. using qdr.fnd.core.pqueue.Enums;
  4. using qdr.fnd.core.pqueue.Exceptions;
  5. using qdr.fnd.core.pqueue.Extensions;
  6. using qdr.fnd.core.pqueue.Process;
  7. using qdr.fnd.core.pqueue.Storages;
  8. using Quadarax.Foundation.Core.Data;
  9. using Quadarax.Foundation.Core.Logging;
  10. using Quadarax.Foundation.Core.Thread;
  11. using Quadarax.Foundation.Core.Value.Extensions;
  12. using Quadarax.Foundation.Core.Value.Generators;
  13. namespace qdr.fnd.core.pqueue
  14. {
  15. public class PsQueue : LoopWorker, IPsQueue
  16. {
  17. #region *** Constants ***
  18. private const string CS_DEF_QUEUE_NAME = "PSQUEUE";
  19. public const int CN_PRIO_SYSTEM = 0;
  20. public const int CN_PRIO_IMMEDIATE = 2;
  21. public const int CN_PRIO_CUSTOM = 5;
  22. #endregion
  23. #region *** Properties ***
  24. public PsQueueConfiguration Configuration { get;}
  25. public event PsQueueStatusChangedEventHandler? ProcessItemStatusChanged;
  26. #endregion
  27. #region *** Private Fields ***
  28. private TinyHash _hashGenerator;
  29. protected IPsQueueStorage _storage;
  30. private List<Tuple<PsStatusEnum,PsStatusEnum[]>> _statusTransitions = new List<Tuple<PsStatusEnum, PsStatusEnum[]>>();
  31. private ConcurrentDictionary<string, IPsProcessProvider> _workingItems = new ConcurrentDictionary<string, IPsProcessProvider>();
  32. #endregion
  33. #region *** Constructors ***
  34. public PsQueue(string queueName, PsQueueConfiguration configuration,IPsQueueStorage storageProvider, ILogger logger) : base(queueName,null)
  35. {
  36. _hashGenerator = new TinyHash(5);
  37. if (string.IsNullOrEmpty(queueName))
  38. queueName =CS_DEF_QUEUE_NAME + _hashGenerator.NewId();
  39. Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
  40. _storage = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider));
  41. _statusTransitions.Add(new Tuple<PsStatusEnum, PsStatusEnum[]>(PsStatusEnum.New,new[]
  42. {
  43. PsStatusEnum.Pending
  44. }));
  45. _statusTransitions.Add(new Tuple<PsStatusEnum, PsStatusEnum[]>(PsStatusEnum.Pending,new[]
  46. {
  47. PsStatusEnum.Started,
  48. PsStatusEnum.Expired
  49. }));
  50. _statusTransitions.Add(new Tuple<PsStatusEnum, PsStatusEnum[]>(PsStatusEnum.Started,new[]
  51. {
  52. PsStatusEnum.Pending,
  53. PsStatusEnum.Paused,
  54. PsStatusEnum.Stopped,
  55. PsStatusEnum.DoneOk,
  56. PsStatusEnum.DoneFailed,
  57. PsStatusEnum.Expired
  58. }));
  59. _statusTransitions.Add(new Tuple<PsStatusEnum, PsStatusEnum[]>(PsStatusEnum.Paused,new[]
  60. {
  61. PsStatusEnum.Started,
  62. PsStatusEnum.Stopped,
  63. PsStatusEnum.Expired
  64. }));
  65. _statusTransitions.Add(new Tuple<PsStatusEnum, PsStatusEnum[]>(PsStatusEnum.Stopped,new[]
  66. {
  67. PsStatusEnum.DoneFailed,
  68. PsStatusEnum.Expired
  69. }));
  70. _statusTransitions.Add(new Tuple<PsStatusEnum, PsStatusEnum[]>(PsStatusEnum.DoneOk,new[]
  71. {
  72. PsStatusEnum.Deleted
  73. }));
  74. _statusTransitions.Add(new Tuple<PsStatusEnum, PsStatusEnum[]>(PsStatusEnum.DoneFailed,new[]
  75. {
  76. PsStatusEnum.Deleted
  77. }));
  78. _statusTransitions.Add(new Tuple<PsStatusEnum, PsStatusEnum[]>(PsStatusEnum.Expired,new[]
  79. {
  80. PsStatusEnum.Deleted
  81. }));
  82. _statusTransitions.Add(new Tuple<PsStatusEnum, PsStatusEnum[]>(PsStatusEnum.Deleted,Array.Empty<PsStatusEnum>()));
  83. }
  84. #endregion
  85. #region *** Public Operations ***
  86. public IQueryable<IPsQueueItem> Query(string? owner, string? ownerRef, params Tuple<bool,PsStatusEnum>[] status)
  87. {
  88. throw new NotImplementedException();
  89. }
  90. /// <inheritdoc/>
  91. public string EnQueueItem(IPsQueueItemDescriptor item)
  92. {
  93. ValidateItem(item);
  94. Log(LogSeverityEnum.Debug,$"{Name} - Item {item} begins to enqueue.");
  95. var itemId = _storage.CreateItem(item.Owner, item.OwnerRef, item.ProcessProviderClass, item.AtteptsInitial, null);
  96. foreach (var argument in item.Arguments)
  97. {
  98. if (argument.ValueType.FullName == null) throw new InvalidOperationException($"Cannot obtain fullName of type '{argument.ValueType.Name}'");
  99. _storage.CreateItemAttribute(itemId, argument.Name, argument.Value, argument.ValueType.FullName, argument.IsOutput);
  100. Log(LogSeverityEnum.Debug,$"{Name} - Attribute {argument.Name} was append to item [{itemId}]");
  101. }
  102. var newItem = GetItemInternal(itemId);
  103. SetItemStatusInternal(newItem, PsStatusEnum.Pending, true, "Item was enqueued.");
  104. Log(LogSeverityEnum.Info,$"{Name} - Item [{itemId}] was enqueued as {newItem}");
  105. return itemId;
  106. }
  107. /// <inheritdoc/>
  108. public IPsQueueItem GetQueueItem(string itemId)
  109. {
  110. ValidateItemId(itemId);
  111. Log(LogSeverityEnum.Debug,$"{Name} - Item [{itemId}] begins to get.");
  112. var item = GetItemInternal(itemId);
  113. Log(LogSeverityEnum.Info,$"{Name} - Item [{itemId}] was fetched as {item}");
  114. return item;
  115. }
  116. /// <inheritdoc/>
  117. public void ForceStartQueueItem(string itemId, string emitter)
  118. {
  119. ValidateItemId(itemId);
  120. ValidateEmitter(emitter);
  121. Log(LogSeverityEnum.Debug, $"{Name} - Item [{itemId}] begins to schedule for force start by '{emitter}'.");
  122. var item = _storage.GetItem(itemId);
  123. EnsureItemStateExpired(item);
  124. if (item.Status.In(PsStatusEnum.Started, PsStatusEnum.Stopped, PsStatusEnum.Expired))
  125. throw new PSQueueValidationException(PSQueueValidationException.ErrorCodes.InvalidStatusForForceStart, item.Status);
  126. _storage.UpdateItemValidity(itemId, DateTime.Now, item.ValidTo == null ? null : DateTime.Now.Add(item.ValidTo.Value - item.ValidFrom));
  127. _storage.UpdateItemPriority(itemId, CN_PRIO_IMMEDIATE);
  128. SetItemStatusInternal(item, PsStatusEnum.Pending, true, $"Item will reset due force start by emitter '{emitter}'.");
  129. Log(LogSeverityEnum.Info, $"{Name} - Item [{itemId}] was scheduled for force start by {emitter}.");
  130. }
  131. /// <inheritdoc/>
  132. public void ForceStopQueueItem(string itemId, string emitter)
  133. {
  134. ValidateItemId(itemId);
  135. ValidateEmitter(emitter);
  136. Log(LogSeverityEnum.Debug, $"{Name} - Item [{itemId}] begins to schedule for stop start by '{emitter}'.");
  137. var item = _storage.GetItem(itemId);
  138. EnsureItemStateExpired(item);
  139. if (item.Status.In(PsStatusEnum.Expired, PsStatusEnum.DoneOk, PsStatusEnum.DoneFailed))
  140. throw new PSQueueValidationException(PSQueueValidationException.ErrorCodes.InvalidStatusForForceStop, item.Status);
  141. _storage.UpdateItemValidity(itemId, DateTime.Now, item.ValidTo == null ? null : DateTime.Now.Add(item.ValidTo.Value - item.ValidFrom));
  142. _storage.UpdateItemPriority(itemId, CN_PRIO_IMMEDIATE);
  143. SetItemStatusInternal(item, PsStatusEnum.Stopped, true, $"Item will reset due force stop by emitter '{emitter}'.");
  144. Log(LogSeverityEnum.Info, $"{Name} - Item [{itemId}] was scheduled for force stop by {emitter}.");
  145. }
  146. /// <inheritdoc/>
  147. public void PauseQueueItem(string itemId, string emitter)
  148. {
  149. ValidateItemId(itemId);
  150. ValidateEmitter(emitter);
  151. Log(LogSeverityEnum.Debug, $"{Name} - Item [{itemId}] begins to schedule for pause by '{emitter}'.");
  152. var item = _storage.GetItem(itemId);
  153. EnsureItemStateExpired(item);
  154. if (item.Status.In(PsStatusEnum.Expired, PsStatusEnum.DoneOk, PsStatusEnum.DoneFailed))
  155. throw new PSQueueValidationException(PSQueueValidationException.ErrorCodes.InvalidStatusForPause, item.Status);
  156. _storage.UpdateItemValidity(itemId, DateTime.Now, item.ValidTo == null ? null : DateTime.Now.Add(item.ValidTo.Value - item.ValidFrom));
  157. _storage.UpdateItemPriority(itemId, CN_PRIO_IMMEDIATE);
  158. SetItemStatusInternal(item, PsStatusEnum.Paused, true, $"Item will reset due pause by emitter '{emitter}'.");
  159. Log(LogSeverityEnum.Info, $"{Name} - Item [{itemId}] was scheduled for pause by {emitter}.");
  160. }
  161. public void StartQueueItem(string itemId, string emmiter, bool isImmediate)
  162. {
  163. throw new NotImplementedException();
  164. }
  165. /// <inheritdoc/>
  166. public IQueryable<IPsQueueItem> QueryQueueItems(IPaging paging, string? owner, string? ownerRef, bool includeTransient = false, params PsStatusEnum[] statuses)
  167. {
  168. if (paging == null)
  169. throw new ArgumentNullException(nameof(paging));
  170. var statusesForQuery = new List<Tuple<bool, PsStatusEnum>>();
  171. foreach (var status in statuses)
  172. {
  173. statusesForQuery.Add(new Tuple<bool, PsStatusEnum>(false, status));
  174. if (includeTransient)
  175. statusesForQuery.Add(new Tuple<bool, PsStatusEnum>(true, status));
  176. }
  177. return _storage.Query(owner, ownerRef, statusesForQuery.ToArray());
  178. }
  179. public void Start()
  180. {
  181. base.Start();
  182. }
  183. public void Stop()
  184. {
  185. base.Stop();
  186. }
  187. #endregion
  188. #region *** Private Operations ***
  189. #region Overrides of LoopWorker
  190. /// <inheritdoc />
  191. protected override Task DoIteration(IWorkerContext? context)
  192. {
  193. // fetch items to immediate stop - frees workers
  194. var itemsToStop = Query(null, null,
  195. new Tuple<bool, PsStatusEnum>[] { new(true, PsStatusEnum.Stopped),new(true, PsStatusEnum.Paused) })
  196. .OrderBy(x=>x.Priority).ThenBy(x=>x.ValidFrom);
  197. foreach(var item in itemsToStop){
  198. var process = _workingItems.TryGetValue(item.Id, out var processProvider) ? processProvider : null;
  199. if (process == null)
  200. Log(LogSeverityEnum.Warn, $"{Name} - Cannot find process provider in working cache for item [{item.Id}]. Skiped.");
  201. if (process != null)
  202. {
  203. process.Stop();
  204. SetItemStatusInternal(item, item.Status, false, "Item was stopped by schedule.");
  205. _workingItems.TryRemove(item.Id, out _);
  206. }
  207. }
  208. // fetch working, stopped, paused items which currently expired - frees workers
  209. var itemsToExpire = Query(null, null,
  210. new Tuple<bool, PsStatusEnum>[] { new(true, PsStatusEnum.Stopped),new(true, PsStatusEnum.Paused), new(true, PsStatusEnum.Started) })
  211. .Where(x=>x.IsValiditySpanExpired());
  212. foreach(var item in itemsToExpire)
  213. {
  214. EnsureItemStateExpired(item);
  215. }
  216. // fetch items to immediate start
  217. var workersLeft = Configuration.MaxConcurrentProcesses - _workingItems.Count;
  218. if (workersLeft>0)
  219. {
  220. // fetch
  221. var itemsToProcess = Query(null, null,
  222. new Tuple<bool, PsStatusEnum>[] { new(true, PsStatusEnum.Started) })
  223. .Where(x => !x.IsValiditySpanExpired()).Take(workersLeft);
  224. foreach(var item in itemsToProcess)
  225. {
  226. if (item.AttemptsLeft <= 0)
  227. {
  228. SetItemStatusInternal(item, PsStatusEnum.DoneFailed, true, "Item will be marked as failed due no attempts left.");
  229. SetItemStatusInternal(item, PsStatusEnum.DoneFailed, false, "Item will be marked as failed due no attempts left.");
  230. continue;
  231. }
  232. _storage.UpdateItemAttempts(item.Id, item.AttemptsLeft - 1);
  233. var provider = ResolveProvider(item.ProcessProviderClass, item.Arguments);
  234. _storage.UpdateItemStarted(item.Id, DateTime.Now);
  235. _storage.UpdateItemFinished(item.Id, null);
  236. _workingItems.TryAdd(item.Id, provider);
  237. provider.Start();
  238. SetItemStatusInternal(item, PsStatusEnum.Started, false, "Item was started by schedule.");
  239. }
  240. }
  241. // fetch items to close expired
  242. var itemsToClose = Query(null, null,
  243. new Tuple<bool, PsStatusEnum>[] { new(true, PsStatusEnum.Expired) });
  244. foreach (var item in itemsToClose)
  245. {
  246. SetItemStatusInternal(item, PsStatusEnum.Expired, false, "Item will be expired by schedule.");
  247. }
  248. // fetch pending items to initiate to expired
  249. var itemsPendingToExpire = Query(null, null,
  250. new Tuple<bool, PsStatusEnum>[] { new(true, PsStatusEnum.Pending), new(false, PsStatusEnum.Pending) });
  251. foreach(var item in itemsPendingToExpire)
  252. {
  253. SetItemStatusInternal(item, PsStatusEnum.Expired, true, "Item will be expired by schedule.");
  254. }
  255. return base.DoIteration(context);
  256. }
  257. #endregion
  258. protected void ValidateItemId(string itemId)
  259. {
  260. if (string.IsNullOrEmpty(itemId))
  261. throw new ArgumentNullException(nameof(itemId));
  262. }
  263. protected void ValidateEmitter(string emitter)
  264. {
  265. if (string.IsNullOrEmpty(emitter))
  266. throw new ArgumentNullException(nameof(emitter));
  267. }
  268. protected void ValidateItem(IPsQueueItemDescriptor item)
  269. {
  270. if (item is null)
  271. throw new ArgumentNullException(nameof(item));
  272. if (string.IsNullOrEmpty(item.Owner))
  273. throw new PSQueueValidationException(PSQueueValidationException.ErrorCodes.OwnerRequired);
  274. if (string.IsNullOrEmpty(item.OwnerRef))
  275. throw new PSQueueValidationException(PSQueueValidationException.ErrorCodes.OwnerRefRequired);
  276. var provider = ResolveProvider(item.ProcessProviderClass, item.Arguments);
  277. provider.Validate();
  278. if (item.AtteptsInitial <= 0)
  279. throw new PSQueueValidationException(PSQueueValidationException.ErrorCodes.InitialAttemptMustBePositive);
  280. }
  281. protected void SetItemStatusInternal(IPsQueueItem item, PsStatusEnum? newStatus, bool isTransient, string message)
  282. {
  283. var oldStatus = item.Status;
  284. var oldIsTransient = item.IsStatusTransient;
  285. if (!isTransient && newStatus == null)
  286. throw new PSQueueValidationException(PSQueueValidationException.ErrorCodes.InvalidStatusTransientCombination);
  287. if (newStatus != null)
  288. {
  289. if (!_statusTransitions.First(x => x.Item1 == oldStatus).Item2.Contains(newStatus.Value))
  290. throw new PSQueueValidationException(PSQueueValidationException.ErrorCodes.InvalidStatusTransition, oldStatus,newStatus ?? oldStatus, string.Join(",", _statusTransitions.First(x => x.Item1 == oldStatus).Item2.Select(x=>x.ToString())));
  291. }
  292. _storage.AppendStatusJournal(item.Id, newStatus ?? oldStatus, isTransient, message);
  293. _storage.UpdateItemStatus(item.Id, newStatus, isTransient);
  294. ((PsQueueItem)item).SetStatus(newStatus, isTransient, message);
  295. ProcessItemStatusChanged?.Invoke(this, new PsQueueStatusChangedEventArgs(item.Id) { CurrentStatus = newStatus ?? oldStatus, CurrentIsStatusTransient = isTransient, PreviousStatus = oldStatus, PreviousIsStatusTransient = oldIsTransient });
  296. Log(LogSeverityEnum.Debug, $"{Name} - Item [{item.Id}] changed status '{oldStatus}[{oldIsTransient}]' to '{(newStatus ?? oldStatus)}[{isTransient}]'.");
  297. }
  298. protected IPsProcessProvider ResolveProvider(string providerClass, IEnumerable<ITypedArgument> arguments)
  299. {
  300. if (string.IsNullOrEmpty(providerClass))
  301. throw new PSQueueValidationException(PSQueueValidationException.ErrorCodes.ProcessProviderRequired);
  302. var providerType = Type.GetType(providerClass);
  303. if (providerType == null)
  304. throw new PSQueueValidationException(PSQueueValidationException.ErrorCodes.ProcessProviderCannotResolve, providerClass);
  305. var provider = Activator.CreateInstance(providerType, arguments) as IPsProcessProvider;
  306. if (provider == null)
  307. throw new PSQueueValidationException(PSQueueValidationException.ErrorCodes.ProcessProviderCannotResolve, providerClass);
  308. return provider;
  309. }
  310. protected void EnsureItemStateExpired(IPsQueueItem item)
  311. {
  312. if (item==null)
  313. throw new ArgumentNullException(nameof(item));
  314. if (item.IsValiditySpanExpired() && item.Status != PsStatusEnum.Expired)
  315. {
  316. if (item.Status == PsStatusEnum.Started)
  317. {
  318. if (!_workingItems.TryGetValue(item.Id, out var process))
  319. throw new InvalidOperationException($"Cannot find process provider in working cache for item [{item.Id}]");
  320. SetItemStatusInternal(item, PsStatusEnum.Stopped, true, "Item will be stopped because expired.");
  321. SetItemStatusInternal(item, PsStatusEnum.Stopped, false, "Item was stopped by system because expired.");
  322. // NOTE: fast switch to stopped due prevents main loop to handle - stops later
  323. _workingItems.TryRemove(item.Id, out _);
  324. process.Stop();
  325. SetItemStatusInternal(item, PsStatusEnum.Expired, true, "Item will be expired by process.");
  326. }
  327. else
  328. {
  329. SetItemStatusInternal(item, PsStatusEnum.Expired, true, "Item will be expired by process.");
  330. }
  331. item = GetItemInternal(item.Id);
  332. }
  333. }
  334. protected IPsQueueItem GetItemInternal(string itemId)
  335. {
  336. ValidateItemId(itemId);
  337. var item = _storage.GetItem(itemId);
  338. return item;
  339. }
  340. protected override void OnDisposing()
  341. {
  342. base.OnDisposing();
  343. ProcessItemStatusChanged = null;
  344. _storage.Dispose();
  345. //TODO: Implement OnDisposing
  346. foreach (var item in _workingItems.Values)
  347. item.Stop();
  348. _workingItems.Clear();
  349. _workingItems = null!;
  350. }
  351. #endregion
  352. }
  353. }