S1DashboardRebuildWorker.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. using Admin.NET.Plugin.AiDOP.Order;
  2. using Microsoft.Extensions.DependencyInjection;
  3. using Microsoft.Extensions.Hosting;
  4. using Microsoft.Extensions.Logging;
  5. namespace Admin.NET.Plugin.AiDOP.DataPlatform.S1Refresh;
  6. public sealed class S1DashboardRebuildWorker : BackgroundService
  7. {
  8. private static readonly TimeSpan IdleDelay = TimeSpan.FromSeconds(5);
  9. private static readonly TimeSpan HeartbeatInterval = TimeSpan.FromSeconds(30);
  10. private readonly IServiceScopeFactory _scopeFactory;
  11. private readonly S1DashboardRebuildQueue _queue;
  12. private readonly ILogger _logger;
  13. public S1DashboardRebuildWorker(
  14. IServiceScopeFactory scopeFactory,
  15. S1DashboardRebuildQueue queue,
  16. ILoggerFactory loggerFactory)
  17. {
  18. _scopeFactory = scopeFactory;
  19. _queue = queue;
  20. _logger = loggerFactory.CreateLogger(nameof(S1DashboardRebuildWorker));
  21. }
  22. protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  23. {
  24. try { await Task.Delay(TimeSpan.FromSeconds(8), stoppingToken); }
  25. catch (OperationCanceledException) { return; }
  26. using (var probe = _scopeFactory.CreateScope())
  27. {
  28. var capability = probe.ServiceProvider.GetService<Admin.NET.Plugin.AiDOP.DataPlatform.MdpRebuild.IModuleRebuildCapability>();
  29. if (capability?.IsEnabled("S1") == true)
  30. {
  31. _logger.LogInformation("[S1DashboardRebuildWorker] S1 已迁入通用 ModuleRebuildWorker,本 Worker 不再领取任务");
  32. try { await Task.Delay(Timeout.InfiniteTimeSpan, stoppingToken); }
  33. catch (OperationCanceledException) { }
  34. return;
  35. }
  36. }
  37. await FailStaleAsync(stoppingToken);
  38. var running = new List<Task>();
  39. while (!stoppingToken.IsCancellationRequested)
  40. {
  41. try
  42. {
  43. await FillSlotsAsync(running, stoppingToken);
  44. }
  45. catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
  46. {
  47. break;
  48. }
  49. catch (Exception ex)
  50. {
  51. _logger.LogWarning(ex, "[S1DashboardRebuildWorker] run failed");
  52. }
  53. running.RemoveAll(t => t.IsCompleted);
  54. if (running.Count == 0)
  55. {
  56. try
  57. {
  58. using var linked = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
  59. linked.CancelAfter(IdleDelay);
  60. try { await _queue.Reader.ReadAsync(linked.Token); }
  61. catch (OperationCanceledException) when (!stoppingToken.IsCancellationRequested) { }
  62. }
  63. catch (OperationCanceledException) { break; }
  64. }
  65. else
  66. {
  67. var pulse = WaitPulseAsync(stoppingToken);
  68. var completed = await Task.WhenAny(running.Append(pulse));
  69. if (completed != pulse)
  70. running.Remove(completed);
  71. }
  72. }
  73. try { await Task.WhenAll(running); }
  74. catch (Exception ex) { _logger.LogWarning(ex, "[S1DashboardRebuildWorker] drain failed"); }
  75. }
  76. private async Task FillSlotsAsync(List<Task> running, CancellationToken stoppingToken)
  77. {
  78. int max = 2;
  79. try
  80. {
  81. max = Math.Clamp(Furion.App.GetConfig<int?>("AiDOP:S1Rebuild:MaxParallelTenants", true) ?? 2, 1, 8);
  82. }
  83. catch
  84. {
  85. max = 2;
  86. }
  87. while (running.Count < max && !stoppingToken.IsCancellationRequested)
  88. {
  89. var started = await TryStartOneAsync(stoppingToken);
  90. if (started == null)
  91. break;
  92. running.Add(started);
  93. }
  94. }
  95. private async Task<Task> TryStartOneAsync(CancellationToken stoppingToken)
  96. {
  97. using var probe = _scopeFactory.CreateScope();
  98. var job = await probe.ServiceProvider.GetRequiredService<IS1DashboardRebuildJobStore>()
  99. .ClaimNextQueuedAsync(stoppingToken);
  100. if (job == null)
  101. return null;
  102. return ExecuteClaimedAsync(job, stoppingToken);
  103. }
  104. private async Task ExecuteClaimedAsync(Admin.NET.Plugin.AiDOP.Entity.SmartOps.AdoS1DashboardRebuildJob job, CancellationToken stoppingToken)
  105. {
  106. using var scope = _scopeFactory.CreateScope();
  107. var svc = scope.ServiceProvider.GetRequiredService<S1DashboardRebuildService>();
  108. var transform = scope.ServiceProvider.GetRequiredService<S1MdpSyncTransformService>();
  109. using var heartbeatCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
  110. var heartbeat = KeepJobHeartbeatAsync(job.Id, heartbeatCts.Token);
  111. try
  112. {
  113. await svc.RunClaimedAsync(
  114. job,
  115. (runScope, rebuildJobId, report, ct) =>
  116. transform.RunFullAsync(runScope, ct, "MANUAL", rebuildJobId, report),
  117. stoppingToken);
  118. }
  119. finally
  120. {
  121. heartbeatCts.Cancel();
  122. try { await heartbeat; } catch (OperationCanceledException) { }
  123. }
  124. }
  125. private async Task KeepJobHeartbeatAsync(long jobId, CancellationToken ct)
  126. {
  127. while (!ct.IsCancellationRequested)
  128. {
  129. try
  130. {
  131. await Task.Delay(HeartbeatInterval, ct);
  132. using var scope = _scopeFactory.CreateScope();
  133. await scope.ServiceProvider.GetRequiredService<S1DashboardRebuildService>().TouchHeartbeatAsync(jobId, CancellationToken.None);
  134. }
  135. catch (OperationCanceledException)
  136. {
  137. return;
  138. }
  139. catch (Exception ex)
  140. {
  141. _logger.LogWarning(ex, "[S1DashboardRebuildWorker] job heartbeat failed jobId={JobId}", jobId);
  142. }
  143. }
  144. }
  145. private async Task WaitPulseAsync(CancellationToken stoppingToken)
  146. {
  147. using var linked = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
  148. linked.CancelAfter(IdleDelay);
  149. try { await _queue.Reader.ReadAsync(linked.Token); }
  150. catch (OperationCanceledException) { }
  151. }
  152. private async Task FailStaleAsync(CancellationToken stoppingToken)
  153. {
  154. try
  155. {
  156. using var scope = _scopeFactory.CreateScope();
  157. await scope.ServiceProvider.GetRequiredService<S1DashboardRebuildService>().FailStaleAsync(stoppingToken);
  158. }
  159. catch (Exception ex)
  160. {
  161. _logger.LogWarning(ex, "[S1DashboardRebuildWorker] fail-stale failed");
  162. }
  163. }
  164. }