MdpRefreshDispatcher.cs 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. using Microsoft.Extensions.DependencyInjection;
  2. using Microsoft.Extensions.Logging;
  3. namespace Admin.NET.Plugin.AiDOP.Infrastructure;
  4. /// <summary>
  5. /// 数据中台全量刷新后台派发器。
  6. /// 将评审 / 采购管线末尾的全量 ETL 从「同步 await」改为「后台异步执行」,
  7. /// 并按 key(如 "S1"、"S4")做「单飞 + 合并去抖」:
  8. /// · 该 key 空闲 → 立即后台执行一轮;
  9. /// · 正在执行 → 只置 pending,不排队叠加;
  10. /// · 一轮结束若 pending 为真 → 再补跑一轮(吸收运行期间累计的触发)。
  11. /// 说明:进程内单飞;多实例部署时跨实例合并依赖各模块的定时刷新任务兜底。
  12. /// </summary>
  13. public class MdpRefreshDispatcher : ISingleton
  14. {
  15. private readonly IServiceScopeFactory _scopeFactory;
  16. private readonly ILogger<MdpRefreshDispatcher> _logger;
  17. private readonly object _gate = new();
  18. private readonly Dictionary<string, RefreshState> _states = new();
  19. public MdpRefreshDispatcher(IServiceScopeFactory scopeFactory, ILogger<MdpRefreshDispatcher> logger)
  20. {
  21. _scopeFactory = scopeFactory;
  22. _logger = logger;
  23. }
  24. /// <summary>
  25. /// 提交一次后台刷新(立即返回,不等待执行完成)。
  26. /// </summary>
  27. /// <param name="key">去抖分组键,同一 key 同一时刻最多 1 个在跑。</param>
  28. /// <param name="job">在独立 DI scope 内执行的刷新委托。</param>
  29. public void Enqueue(string key, Func<IServiceProvider, CancellationToken, Task> job)
  30. {
  31. lock (_gate)
  32. {
  33. if (_states.TryGetValue(key, out var state) && state.Running)
  34. {
  35. state.Pending = true; // 合并:运行期间的新触发只置 pending
  36. return;
  37. }
  38. _states[key] = new RefreshState { Running = true, Pending = false };
  39. }
  40. // fire-and-forget:独立后台任务,异常在内部吞掉并记日志
  41. _ = RunLoopAsync(key, job);
  42. }
  43. private async Task RunLoopAsync(string key, Func<IServiceProvider, CancellationToken, Task> job)
  44. {
  45. while (true)
  46. {
  47. try
  48. {
  49. using var scope = _scopeFactory.CreateScope();
  50. await job(scope.ServiceProvider, CancellationToken.None);
  51. }
  52. catch (Exception ex)
  53. {
  54. _logger.LogError(ex, "MdpRefreshDispatcher[{Key}] 后台全量刷新失败", key);
  55. }
  56. lock (_gate)
  57. {
  58. var state = _states[key];
  59. if (!state.Pending)
  60. {
  61. state.Running = false;
  62. return;
  63. }
  64. state.Pending = false; // 吸收运行期间累计的触发,再补跑一轮
  65. }
  66. }
  67. }
  68. private sealed class RefreshState
  69. {
  70. public bool Running;
  71. public bool Pending;
  72. }
  73. }