S1DashboardRebuildService.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. using Admin.NET.Plugin.AiDOP.DataPlatform.MdpRebuild;
  2. using Admin.NET.Plugin.AiDOP.Entity.SmartOps;
  3. using Admin.NET.Plugin.AiDOP.Order;
  4. using Microsoft.Extensions.Logging;
  5. using Microsoft.Extensions.Logging.Abstractions;
  6. namespace Admin.NET.Plugin.AiDOP.DataPlatform.S1Refresh;
  7. public sealed class S1DashboardRebuildService : ITransient
  8. {
  9. private readonly IS1DashboardRebuildJobStore _jobs;
  10. private readonly S1DashboardRebuildQueue _queue;
  11. private readonly ModuleRebuildService _moduleRebuild;
  12. private readonly IModuleRebuildCapability _capability;
  13. private readonly ILogger _logger;
  14. public S1DashboardRebuildService(IS1DashboardRebuildJobStore jobs, S1DashboardRebuildQueue queue)
  15. : this(jobs, queue, null, new AlwaysOnModuleRebuildCapability(), NullLoggerFactory.Instance)
  16. {
  17. }
  18. public S1DashboardRebuildService(
  19. IS1DashboardRebuildJobStore jobs,
  20. S1DashboardRebuildQueue queue,
  21. ILoggerFactory loggerFactory)
  22. : this(jobs, queue, null, new AlwaysOnModuleRebuildCapability(), loggerFactory)
  23. {
  24. }
  25. public S1DashboardRebuildService(
  26. IS1DashboardRebuildJobStore jobs,
  27. S1DashboardRebuildQueue queue,
  28. ModuleRebuildService moduleRebuild,
  29. IModuleRebuildCapability capability,
  30. ILoggerFactory loggerFactory)
  31. {
  32. _jobs = jobs;
  33. _queue = queue;
  34. _moduleRebuild = moduleRebuild;
  35. _capability = capability;
  36. _logger = loggerFactory.CreateLogger(nameof(S1DashboardRebuildService));
  37. }
  38. private bool UseUnified => _moduleRebuild != null && _capability.IsEnabled("S1");
  39. public async Task<(int StatusCode, S1RebuildJobAccepted Body)> EnqueueAsync(
  40. long tenantId,
  41. long factoryId,
  42. long? requestedBy,
  43. string triggerType = "MANUAL",
  44. CancellationToken ct = default)
  45. {
  46. if (UseUnified)
  47. {
  48. var (status, body) = await _moduleRebuild.EnqueueAsync("S1", tenantId, factoryId, requestedBy, triggerType, ct);
  49. return (status, new S1RebuildJobAccepted
  50. {
  51. Ok = body.Ok,
  52. JobId = body.JobId,
  53. Status = body.Status,
  54. Message = body.Message
  55. });
  56. }
  57. var scope = S1MdpRunScope.Create(tenantId, factoryId);
  58. var active = await _jobs.FindActiveAsync(scope.TenantId, scope.FactoryId, ct);
  59. if (active != null)
  60. {
  61. return (409, new S1RebuildJobAccepted
  62. {
  63. Ok = false,
  64. JobId = active.Id,
  65. Status = active.Status,
  66. Message = "S1 数据重算正在执行,请勿重复提交"
  67. });
  68. }
  69. var now = DateTime.Now;
  70. var created = await _jobs.InsertQueuedAsync(new AdoS1DashboardRebuildJob
  71. {
  72. TenantId = scope.TenantId,
  73. FactoryId = scope.FactoryId,
  74. Status = S1DashboardRebuildStatus.Queued,
  75. CurrentStage = S1MdpRebuildStage.Queued,
  76. StageIndex = 0,
  77. StageTotal = S1MdpRebuildStage.StageTotal,
  78. ProgressPercent = 0,
  79. ProgressMessage = "已入队",
  80. LastProgressAt = now,
  81. TriggerType = string.IsNullOrWhiteSpace(triggerType) ? "MANUAL" : triggerType,
  82. RequestedBy = requestedBy,
  83. SubmittedAt = now,
  84. CreateTime = now,
  85. UpdateTime = now
  86. }, ct);
  87. var oldest = await _jobs.FindActiveAsync(scope.TenantId, scope.FactoryId, ct);
  88. if (oldest != null && oldest.Id != created.Id)
  89. {
  90. created.Status = S1DashboardRebuildStatus.Failed;
  91. created.CurrentStage = S1MdpRebuildStage.Failed;
  92. created.FinishedAt = DateTime.Now;
  93. created.ErrorMessage = "S1 数据重算正在执行,请勿重复提交";
  94. created.UpdateTime = DateTime.Now;
  95. await _jobs.UpdateAsync(created, ct);
  96. return (409, new S1RebuildJobAccepted
  97. {
  98. Ok = false,
  99. JobId = oldest.Id,
  100. Status = oldest.Status,
  101. Message = "S1 数据重算正在执行,请勿重复提交"
  102. });
  103. }
  104. _queue.Pulse();
  105. return (202, new S1RebuildJobAccepted
  106. {
  107. Ok = true,
  108. JobId = created.Id,
  109. Status = S1DashboardRebuildStatus.Queued,
  110. Message = "S1 数据重算已排队"
  111. });
  112. }
  113. public async Task<S1RebuildJobDto> GetAsync(long jobId, long tenantId, long factoryId, CancellationToken ct = default)
  114. {
  115. if (UseUnified)
  116. {
  117. try
  118. {
  119. var dto = await _moduleRebuild.GetAsync("S1", jobId, tenantId, factoryId, ct);
  120. return Map(dto);
  121. }
  122. catch
  123. {
  124. // 兼容期内回退旧表
  125. }
  126. }
  127. var scope = S1MdpRunScope.Create(tenantId, factoryId);
  128. var row = await _jobs.GetByIdAsync(jobId, scope.TenantId, scope.FactoryId, ct);
  129. if (row == null)
  130. throw Oops.Oh("任务不存在");
  131. return ToDto(row);
  132. }
  133. public async Task<S1RebuildJobDto> LatestAsync(long tenantId, long factoryId, CancellationToken ct = default)
  134. {
  135. if (UseUnified)
  136. {
  137. var dto = await _moduleRebuild.LatestAsync("S1", tenantId, factoryId, ct);
  138. if (dto != null) return Map(dto);
  139. }
  140. var scope = S1MdpRunScope.Create(tenantId, factoryId);
  141. var row = await _jobs.GetLatestAsync(scope.TenantId, scope.FactoryId, ct);
  142. return row == null ? null : ToDto(row);
  143. }
  144. public async Task FailStaleAsync(CancellationToken ct = default) =>
  145. await _jobs.FailStaleRunningAsync(S1MdpFullRunLock.StaleAfter, ct);
  146. public async Task TouchHeartbeatAsync(long jobId, CancellationToken ct = default)
  147. {
  148. try
  149. {
  150. await _jobs.TouchHeartbeatAsync(jobId, DateTime.Now, ct);
  151. }
  152. catch (Exception ex)
  153. {
  154. _logger.LogWarning(ex, "[S1Rebuild] heartbeat failed jobId={JobId}", jobId);
  155. }
  156. }
  157. public async Task ApplyProgressAsync(long jobId, S1MdpProgressUpdate update, CancellationToken ct = default)
  158. {
  159. try
  160. {
  161. var now = DateTime.Now;
  162. if (update.CompletedStage != null && update.Rows.HasValue)
  163. {
  164. await _jobs.UpdateStageResultAsync(
  165. jobId, update.CompletedStage, update.Rows.Value, update.ProgressPercent, update.Message, now, ct);
  166. }
  167. await _jobs.UpdateProgressAsync(
  168. jobId, update.Stage, update.StageIndex, update.ProgressPercent, update.Message, now, ct);
  169. }
  170. catch (Exception ex)
  171. {
  172. _logger.LogWarning(ex, "[S1Rebuild] progress write failed jobId={JobId} stage={Stage}", jobId, update.Stage);
  173. }
  174. }
  175. public async Task RunNextAsync(
  176. Func<S1MdpRunScope, long, Func<S1MdpProgressUpdate, Task>, CancellationToken, Task<S1MdpSyncTransformResult>> runFull,
  177. CancellationToken stoppingToken)
  178. {
  179. var job = await _jobs.ClaimNextQueuedAsync(stoppingToken);
  180. if (job == null)
  181. return;
  182. await RunClaimedAsync(job, runFull, stoppingToken);
  183. }
  184. public async Task<AdoS1DashboardRebuildJob> ClaimNextAsync(CancellationToken ct = default) =>
  185. await _jobs.ClaimNextQueuedAsync(ct);
  186. public async Task RunClaimedAsync(
  187. AdoS1DashboardRebuildJob job,
  188. Func<S1MdpRunScope, long, Func<S1MdpProgressUpdate, Task>, CancellationToken, Task<S1MdpSyncTransformResult>> runFull,
  189. CancellationToken stoppingToken)
  190. {
  191. var failedStage = job.CurrentStage;
  192. try
  193. {
  194. var scope = S1MdpRunScope.Create(job.TenantId, job.FactoryId);
  195. await ApplyProgressAsync(job.Id, new S1MdpProgressUpdate(
  196. S1MdpRebuildStage.AcquiringLock, 0, 2, "等待现有 S1 全量任务完成"), CancellationToken.None);
  197. var result = await runFull(
  198. scope,
  199. job.Id,
  200. async update =>
  201. {
  202. failedStage = update.Stage;
  203. await ApplyProgressAsync(job.Id, update, CancellationToken.None);
  204. },
  205. stoppingToken);
  206. var now = DateTime.Now;
  207. job.Status = S1DashboardRebuildStatus.Success;
  208. job.CurrentStage = S1MdpRebuildStage.Success;
  209. job.StageIndex = S1MdpRebuildStage.StageTotal;
  210. job.ProgressPercent = 100;
  211. job.ProgressMessage = "S1 数据重算已完成";
  212. job.FailedStage = null;
  213. job.LastProgressAt = now;
  214. job.BatchId = result.BatchId;
  215. job.TransformRunLogId = result.RunLogId;
  216. job.StageRows = result.StageRows;
  217. job.StandardRows = result.StandardRows;
  218. job.DwdRows = result.DwdRows;
  219. job.KpiRows = result.KpiRows;
  220. job.AtomicRows = result.AtomicRows;
  221. job.FinishedAt = now;
  222. job.HeartbeatAt = now;
  223. job.DurationMs = job.StartedAt.HasValue ? ToDurationMs(now - job.StartedAt.Value) : null;
  224. job.UpdateTime = now;
  225. job.ErrorMessage = null;
  226. await _jobs.UpdateAsync(job, CancellationToken.None);
  227. }
  228. catch (S1MdpAlreadyRunningException)
  229. {
  230. var now = DateTime.Now;
  231. job.Status = S1DashboardRebuildStatus.Queued;
  232. job.CurrentStage = S1MdpRebuildStage.AcquiringLock;
  233. job.StageIndex = 0;
  234. job.ProgressPercent = 2;
  235. job.ProgressMessage = "等待现有 S1 全量任务完成";
  236. job.StartedAt = null;
  237. job.HeartbeatAt = now;
  238. job.LastProgressAt = now;
  239. job.UpdateTime = now;
  240. job.ErrorMessage = null;
  241. await _jobs.UpdateAsync(job, CancellationToken.None);
  242. _queue.Pulse();
  243. }
  244. catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
  245. {
  246. await FailJobAsync(job, "服务停止,任务已取消", S1DashboardRebuildStatus.Cancelled, S1MdpRebuildStage.Cancelled, failedStage);
  247. }
  248. catch (Exception ex)
  249. {
  250. await FailJobAsync(
  251. job,
  252. Truncate(ex.Message, 2000),
  253. S1DashboardRebuildStatus.Failed,
  254. S1MdpRebuildStage.Failed,
  255. failedStage);
  256. }
  257. }
  258. private async Task FailJobAsync(AdoS1DashboardRebuildJob job, string message, string status, string currentStage, string failedStage)
  259. {
  260. var now = DateTime.Now;
  261. job.Status = status;
  262. job.CurrentStage = currentStage;
  263. job.FailedStage = status == S1DashboardRebuildStatus.Failed ? failedStage : job.FailedStage;
  264. job.ProgressMessage = status == S1DashboardRebuildStatus.Failed
  265. ? $"在【{S1MdpRebuildStage.ToChinese(failedStage)}】失败"
  266. : message;
  267. job.FinishedAt = now;
  268. job.HeartbeatAt = now;
  269. job.LastProgressAt = now;
  270. job.DurationMs = job.StartedAt.HasValue ? ToDurationMs(now - job.StartedAt.Value) : null;
  271. job.ErrorMessage = message;
  272. job.UpdateTime = now;
  273. await _jobs.UpdateAsync(job, CancellationToken.None);
  274. }
  275. public static S1RebuildJobDto ToDto(AdoS1DashboardRebuildJob row) => new()
  276. {
  277. JobId = row.Id,
  278. Status = row.Status,
  279. CurrentStage = row.CurrentStage,
  280. StageIndex = row.StageIndex,
  281. StageTotal = row.StageTotal <= 0 ? S1MdpRebuildStage.StageTotal : row.StageTotal,
  282. ProgressPercent = row.ProgressPercent,
  283. ProgressMessage = row.ProgressMessage,
  284. LastProgressAt = row.LastProgressAt,
  285. HeartbeatAt = row.HeartbeatAt,
  286. FailedStage = row.FailedStage,
  287. SubmittedAt = row.SubmittedAt,
  288. StartedAt = row.StartedAt,
  289. FinishedAt = row.FinishedAt,
  290. DurationMs = row.DurationMs,
  291. BatchId = row.BatchId,
  292. StageRows = row.StageRows,
  293. StandardRows = row.StandardRows,
  294. DwdRows = row.DwdRows,
  295. KpiRows = row.KpiRows,
  296. AtomicRows = row.AtomicRows,
  297. ErrorMessage = row.ErrorMessage
  298. };
  299. private static S1RebuildJobDto Map(Admin.NET.Plugin.AiDOP.DataPlatform.MdpRebuild.ModuleRebuildJobDto dto) => new()
  300. {
  301. JobId = dto.JobId,
  302. Status = dto.Status,
  303. CurrentStage = dto.CurrentStage,
  304. StageIndex = dto.StageIndex,
  305. StageTotal = dto.StageTotal,
  306. ProgressPercent = dto.ProgressPercent,
  307. ProgressMessage = dto.ProgressMessage,
  308. LastProgressAt = dto.LastProgressAt,
  309. HeartbeatAt = dto.HeartbeatAt,
  310. FailedStage = dto.FailedStage,
  311. SubmittedAt = dto.SubmittedAt,
  312. StartedAt = dto.StartedAt,
  313. FinishedAt = dto.FinishedAt,
  314. DurationMs = dto.DurationMs,
  315. BatchId = dto.BatchId,
  316. StageRows = dto.StageRows,
  317. StandardRows = dto.StandardRows,
  318. DwdRows = dto.DwdRows,
  319. KpiRows = dto.KpiRows,
  320. AtomicRows = dto.AtomicRows,
  321. ErrorMessage = dto.ErrorMessage
  322. };
  323. private static int ToDurationMs(TimeSpan elapsed)
  324. {
  325. var ms = elapsed.TotalMilliseconds;
  326. if (double.IsNaN(ms) || ms <= 0) return 0;
  327. return ms >= int.MaxValue ? int.MaxValue : (int)ms;
  328. }
  329. private static string Truncate(string value, int max) =>
  330. string.IsNullOrEmpty(value) || value.Length <= max ? value : value[..max];
  331. }