channel_monitor_runner.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "log/slog"
  6. "math/rand/v2"
  7. "sync"
  8. "time"
  9. "github.com/alitto/pond/v2"
  10. )
  11. // MonitorScheduler 调度器接口,供 ChannelMonitorService 在 CRUD 时回调,
  12. // 用 setter 注入避免 service ↔ runner 的 wire 依赖环。
  13. type MonitorScheduler interface {
  14. // Schedule 为指定监控创建(或重置)独立定时任务。
  15. // 当 m.Enabled=false 时等同于 Unschedule(m.ID)。
  16. Schedule(m *ChannelMonitor)
  17. // Unschedule 取消指定监控的定时任务(若存在)。
  18. Unschedule(id int64)
  19. }
  20. // monitorRunnerSvc 抽出 runner 实际依赖的两个 service 方法:
  21. // - 启动时加载 enabled monitor
  22. // - 每次 ticker 触发执行检测
  23. //
  24. // 用接口而非 *ChannelMonitorService 是为了让 runner 单元测试可注入轻量 stub,
  25. // 避免依赖完整的 repo + encryptor 链路。生产实现 *ChannelMonitorService 自然满足。
  26. type monitorRunnerSvc interface {
  27. ListEnabledMonitors(ctx context.Context) ([]*ChannelMonitor, error)
  28. RunCheck(ctx context.Context, id int64) ([]*CheckResult, error)
  29. }
  30. // ChannelMonitorRunner 渠道监控调度器。
  31. //
  32. // 设计:
  33. // - 每个 enabled monitor 对应一个独立 goroutine + ticker(按各自 IntervalSeconds)
  34. // - Start 时一次性加载所有 enabled monitor 并为每个建立任务
  35. // - Service 在 Create/Update/Delete 后通过 MonitorScheduler 接口回调,
  36. // 即时重建/取消对应任务(无需轮询 DB)
  37. // - 实际 HTTP 检测交给 pond 池(容量 monitorWorkerConcurrency),
  38. // 防止突发并发拖垮上游
  39. //
  40. // 历史清理与日聚合维护由 OpsCleanupService 的 cron 触发
  41. // ChannelMonitorService.RunDailyMaintenance(复用 leader lock + heartbeat),
  42. // 不在 runner 职责内。
  43. type ChannelMonitorRunner struct {
  44. svc monitorRunnerSvc
  45. settingService *SettingService
  46. pool pond.Pool
  47. parentCtx context.Context
  48. parentCancel context.CancelFunc
  49. mu sync.Mutex
  50. tasks map[int64]*scheduledMonitor
  51. wg sync.WaitGroup
  52. started bool
  53. stopped bool
  54. // inFlight 跟踪正在执行的 monitor.ID。fire 调度前会检查避免重复提交,
  55. // 防止单次检测耗时 > interval 时同一 monitor 被并发执行。
  56. inFlight map[int64]struct{}
  57. inFlightMu sync.Mutex
  58. }
  59. // scheduledMonitor 单个监控的运行时上下文。
  60. type scheduledMonitor struct {
  61. id int64
  62. name string
  63. interval time.Duration
  64. jitter time.Duration // 每轮 ± [0, jitter] 的均匀随机偏移;0 = 固定间隔
  65. cancel context.CancelFunc
  66. }
  67. // nextDelay 计算下一次触发的等待时长:interval ± [0, jitter] 的均匀随机偏移。
  68. // 校验链路已保证 interval - jitter >= monitorMinIntervalSeconds,
  69. // 这里仍 clamp 一次下限,兜底数据库中违反约束的脏数据。
  70. func (t *scheduledMonitor) nextDelay() time.Duration {
  71. if t.jitter <= 0 {
  72. return t.interval
  73. }
  74. offset := time.Duration(rand.Int64N(int64(2*t.jitter) + 1)) // [0, 2*jitter]
  75. d := t.interval - t.jitter + offset
  76. if floor := monitorMinIntervalSeconds * time.Second; d < floor {
  77. d = floor
  78. }
  79. return d
  80. }
  81. // NewChannelMonitorRunner 构造调度器。Start 在 wire 中调用一次。
  82. // settingService 用于在每次 fire 前读取功能开关;传 nil 时视为总是启用(兼容测试)。
  83. //
  84. // pool 在构造时即建好:避免 Start 在 mu 内赋值、fire/Stop 在 mu 外读取的竞态隐患,
  85. // 且 pond.NewPool 创建本身近似零开销,提前建池不会浪费资源。
  86. func NewChannelMonitorRunner(svc *ChannelMonitorService, settingService *SettingService) *ChannelMonitorRunner {
  87. return newChannelMonitorRunner(svc, settingService)
  88. }
  89. // newChannelMonitorRunner 内部构造,接受最小化接口,便于单元测试注入 stub。
  90. func newChannelMonitorRunner(svc monitorRunnerSvc, settingService *SettingService) *ChannelMonitorRunner {
  91. ctx, cancel := context.WithCancel(context.Background())
  92. return &ChannelMonitorRunner{
  93. svc: svc,
  94. settingService: settingService,
  95. pool: pond.NewPool(monitorWorkerConcurrency),
  96. parentCtx: ctx,
  97. parentCancel: cancel,
  98. tasks: make(map[int64]*scheduledMonitor),
  99. inFlight: make(map[int64]struct{}),
  100. }
  101. }
  102. // Start 加载所有 enabled monitor 并为每个建立独立定时任务。
  103. // 调用方需保证只调一次(wire ProvideChannelMonitorRunner 内只调一次)。
  104. func (r *ChannelMonitorRunner) Start() {
  105. if r == nil || r.svc == nil {
  106. return
  107. }
  108. r.mu.Lock()
  109. if r.started || r.stopped {
  110. r.mu.Unlock()
  111. return
  112. }
  113. r.started = true
  114. r.mu.Unlock()
  115. ctx, cancel := context.WithTimeout(context.Background(), monitorStartupLoadTimeout)
  116. defer cancel()
  117. enabled, err := r.svc.ListEnabledMonitors(ctx)
  118. if err != nil {
  119. slog.Error("channel_monitor: load enabled monitors failed at startup", "error", err)
  120. return
  121. }
  122. for _, m := range enabled {
  123. r.Schedule(m)
  124. }
  125. slog.Info("channel_monitor: runner started", "scheduled_tasks", len(enabled))
  126. }
  127. // Schedule 为指定监控创建(或重置)独立定时任务。
  128. // - m.Enabled=false 或 APIKeyDecryptFailed=true → 等同于 Unschedule(m.ID)
  129. // - 已存在的任务会先被取消再重建(适用于 IntervalSeconds 变更场景)
  130. // - 新任务立即触发首次检测,之后按 IntervalSeconds 周期触发
  131. func (r *ChannelMonitorRunner) Schedule(m *ChannelMonitor) {
  132. if r == nil || m == nil {
  133. return
  134. }
  135. if !m.Enabled || m.APIKeyDecryptFailed {
  136. r.Unschedule(m.ID)
  137. return
  138. }
  139. interval := time.Duration(m.IntervalSeconds) * time.Second
  140. if interval <= 0 {
  141. // Create/Update 已通过 validateInterval 校验区间,正常路径不可能到这里。
  142. // 真触发说明数据库中存在违反约束的数据或校验链路有 bug,记 Error 暴露问题。
  143. slog.Error("channel_monitor: skip schedule for invalid interval",
  144. "monitor_id", m.ID, "interval_seconds", m.IntervalSeconds)
  145. return
  146. }
  147. jitter := time.Duration(m.JitterSeconds) * time.Second
  148. if jitter < 0 {
  149. jitter = 0
  150. }
  151. r.mu.Lock()
  152. if r.stopped {
  153. r.mu.Unlock()
  154. return
  155. }
  156. if !r.started {
  157. // Start 之前调用 Schedule 通常意味着 wire 顺序错乱:
  158. // 当前 wire 顺序是 SetScheduler → Start,CRUD 钩子最早也只能在请求到达时触发,
  159. // 此时 Start 早已完成。出现此分支时把 monitor 信息打出来便于排查,
  160. // 不入队、不缓存——交给运维通过重启或修复 wire 解决。
  161. r.mu.Unlock()
  162. slog.Warn("channel_monitor: schedule before runner started, skip",
  163. "monitor_id", m.ID, "name", m.Name)
  164. return
  165. }
  166. if existing, ok := r.tasks[m.ID]; ok {
  167. existing.cancel()
  168. }
  169. ctx, cancel := context.WithCancel(r.parentCtx)
  170. task := &scheduledMonitor{
  171. id: m.ID,
  172. name: m.Name,
  173. interval: interval,
  174. jitter: jitter,
  175. cancel: cancel,
  176. }
  177. r.tasks[m.ID] = task
  178. r.wg.Add(1)
  179. r.mu.Unlock()
  180. go r.runScheduled(ctx, task)
  181. }
  182. // Unschedule 取消指定监控的定时任务(若存在)。
  183. // 已经在执行中的检测会通过 ctx 取消信号传递。
  184. func (r *ChannelMonitorRunner) Unschedule(id int64) {
  185. if r == nil {
  186. return
  187. }
  188. r.mu.Lock()
  189. task, ok := r.tasks[id]
  190. if ok {
  191. delete(r.tasks, id)
  192. }
  193. r.mu.Unlock()
  194. if ok {
  195. task.cancel()
  196. }
  197. }
  198. // Stop 优雅停止:取消所有任务、关闭池。
  199. func (r *ChannelMonitorRunner) Stop() {
  200. if r == nil {
  201. return
  202. }
  203. r.mu.Lock()
  204. if r.stopped {
  205. r.mu.Unlock()
  206. return
  207. }
  208. r.stopped = true
  209. r.parentCancel()
  210. r.tasks = nil
  211. r.mu.Unlock()
  212. r.wg.Wait()
  213. r.pool.StopAndWait()
  214. }
  215. // runScheduled 单个监控的循环:立即触发首次(满足"新建/启用即跑"),
  216. // 之后按 interval ± jitter 周期触发;ctx 取消即退出。
  217. // 用 timer 而非 ticker:jitter > 0 时每轮等待时长都需要重新随机化。
  218. func (r *ChannelMonitorRunner) runScheduled(ctx context.Context, task *scheduledMonitor) {
  219. defer r.wg.Done()
  220. r.fire(ctx, task)
  221. timer := time.NewTimer(task.nextDelay())
  222. defer timer.Stop()
  223. for {
  224. select {
  225. case <-ctx.Done():
  226. return
  227. case <-timer.C:
  228. r.fire(ctx, task)
  229. timer.Reset(task.nextDelay())
  230. }
  231. }
  232. }
  233. // fire 提交一次检测到 worker 池。功能开关关闭时跳过本次(不取消任务,
  234. // 重新启用时立即恢复);池满或重复在飞时也跳过。
  235. func (r *ChannelMonitorRunner) fire(ctx context.Context, task *scheduledMonitor) {
  236. if r.settingService != nil {
  237. rt := r.settingService.GetChannelMonitorRuntime(ctx)
  238. if !rt.ActiveProbesAllowed() {
  239. return
  240. }
  241. }
  242. if !r.tryAcquireInFlight(task.id) {
  243. slog.Debug("channel_monitor: skip already in-flight",
  244. "monitor_id", task.id, "name", task.name)
  245. return
  246. }
  247. if _, ok := r.pool.TrySubmit(func() {
  248. r.runOne(task.id, task.name)
  249. }); !ok {
  250. // 池满:丢弃本次检测,但必须释放已占用的 inFlight 槽,否则该 monitor 会被永久卡住。
  251. r.releaseInFlight(task.id)
  252. slog.Warn("channel_monitor: worker pool full, skip submission",
  253. "monitor_id", task.id, "name", task.name)
  254. }
  255. }
  256. // tryAcquireInFlight 原子地占用 monitor 的 in-flight 槽。
  257. // 已被占用返回 false(调用方应跳过本次提交)。
  258. func (r *ChannelMonitorRunner) tryAcquireInFlight(id int64) bool {
  259. r.inFlightMu.Lock()
  260. defer r.inFlightMu.Unlock()
  261. if _, exists := r.inFlight[id]; exists {
  262. return false
  263. }
  264. r.inFlight[id] = struct{}{}
  265. return true
  266. }
  267. // releaseInFlight 释放 in-flight 槽。runOne 完成(含 panic recover)后必须调用。
  268. func (r *ChannelMonitorRunner) releaseInFlight(id int64) {
  269. r.inFlightMu.Lock()
  270. delete(r.inFlight, id)
  271. r.inFlightMu.Unlock()
  272. }
  273. // runOne 执行单个监控的检测。普通错误只记日志;API key 解密失败会撤销任务。
  274. // 任务结束时(含 panic recover)必须释放 in-flight 槽。
  275. func (r *ChannelMonitorRunner) runOne(id int64, name string) {
  276. ctx, cancel := context.WithTimeout(context.Background(), monitorRequestTimeout+monitorPingTimeout+monitorRunOneBuffer)
  277. defer cancel()
  278. defer r.releaseInFlight(id)
  279. defer func() {
  280. if rec := recover(); rec != nil {
  281. slog.Error("channel_monitor: runner panic",
  282. "monitor_id", id, "name", name, "panic", rec)
  283. }
  284. }()
  285. if _, err := r.svc.RunCheck(ctx, id); err != nil {
  286. if errors.Is(err, ErrChannelMonitorAPIKeyDecryptFailed) {
  287. r.Unschedule(id)
  288. }
  289. slog.Warn("channel_monitor: run check failed",
  290. "monitor_id", id, "name", name, "error", err)
  291. }
  292. }