feat(maintenance): add policy executor and config provider

ConfigProvider, PolicyExecutor, DBStatsProvider, scheduler и safety; зависимость robfig/cron/v3.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-06-12 13:21:32 +07:00
co-authored by Cursor
parent 07c3de4939
commit 6510a9ca22
9 changed files with 495 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
package maintenance
import (
"context"
"fmt"
"log"
"strings"
"sync"
"time"
"evobgp/internal/jobs"
"github.com/robfig/cron/v3"
)
// StartScheduler enqueues maintenance_policy_run jobs when cron schedules match.
func StartScheduler(ctx context.Context, provider *ConfigProvider, reg *jobs.Registry, tick time.Duration) {
if provider == nil || reg == nil {
return
}
if tick <= 0 {
tick = 30 * time.Second
}
go func() {
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
var mu sync.Mutex
schedules := map[string]cron.Schedule{}
lastFired := map[string]time.Time{}
rebuild := func() {
mu.Lock()
defer mu.Unlock()
schedules = map[string]cron.Schedule{}
for _, p := range provider.Snapshot() {
if p == nil || !p.Enabled || strings.TrimSpace(p.Schedule) == "" {
continue
}
sched, err := parser.Parse(p.Schedule)
if err != nil {
log.Printf("maintenance: invalid cron for policy %s: %v", p.ID, err)
continue
}
schedules[p.ID] = sched
}
}
rebuild()
t := time.NewTicker(tick)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
rebuild()
now := time.Now().UTC()
mu.Lock()
for _, p := range provider.Snapshot() {
if p == nil || !p.Enabled {
continue
}
sched, ok := schedules[p.ID]
if !ok {
continue
}
prev := lastFired[p.ID]
if prev.IsZero() {
prev = now.Add(-time.Minute)
}
next := sched.Next(prev)
if next.After(now) {
continue
}
slot := next.Unix() / 60
if lf, ok := lastFired[p.ID]; ok && lf.Unix()/60 == slot {
continue
}
lastFired[p.ID] = next
dry := p.DryRunEnabled
idem := fmt.Sprintf("maint-%s-%d", p.ID, slot)
key := idem
_, _, _ = reg.Enqueue("", "maintenance_policy_run", &key, nil, map[string]any{
"policy_id": p.ID,
"dry_run": dry,
"trigger": "scheduler",
})
}
mu.Unlock()
}
}
}()
log.Printf("maintenance: policy scheduler started (tick=%s)", tick)
}