PsQueue.cs 21 KB

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