Update: Js 4 Log.html 80%
This commit is contained in:
@@ -1,42 +1,66 @@
|
||||
// Filename: internal/scheduler/scheduler.go
|
||||
// [REVISED] - 用这个更智能的版本完整替换
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt" // [NEW] 导入 fmt
|
||||
"gemini-balancer/internal/repository"
|
||||
"gemini-balancer/internal/service"
|
||||
"gemini-balancer/internal/settings"
|
||||
"gemini-balancer/internal/store"
|
||||
"strconv" // [NEW] 导入 strconv
|
||||
"strings" // [NEW] 导入 strings
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-co-op/gocron"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ... (Scheduler struct 和 NewScheduler 保持不变) ...
|
||||
const LogCleanupTaskTag = "log-cleanup-task"
|
||||
|
||||
type Scheduler struct {
|
||||
gocronScheduler *gocron.Scheduler
|
||||
logger *logrus.Entry
|
||||
statsService *service.StatsService
|
||||
logService *service.LogService
|
||||
settingsManager *settings.SettingsManager
|
||||
keyRepo repository.KeyRepository
|
||||
store store.Store
|
||||
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewScheduler(statsSvc *service.StatsService, keyRepo repository.KeyRepository, logger *logrus.Logger) *Scheduler {
|
||||
func NewScheduler(
|
||||
statsSvc *service.StatsService,
|
||||
logSvc *service.LogService,
|
||||
keyRepo repository.KeyRepository,
|
||||
settingsMgr *settings.SettingsManager,
|
||||
store store.Store,
|
||||
logger *logrus.Logger,
|
||||
) *Scheduler {
|
||||
s := gocron.NewScheduler(time.UTC)
|
||||
s.TagsUnique()
|
||||
return &Scheduler{
|
||||
gocronScheduler: s,
|
||||
logger: logger.WithField("component", "Scheduler📆"),
|
||||
statsService: statsSvc,
|
||||
logService: logSvc,
|
||||
settingsManager: settingsMgr,
|
||||
keyRepo: keyRepo,
|
||||
store: store,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// ... (Start 和 listenForSettingsUpdates 保持不变) ...
|
||||
func (s *Scheduler) Start() {
|
||||
s.logger.Info("Starting scheduler and registering jobs...")
|
||||
|
||||
// 任务一:每小时执行一次的统计聚合
|
||||
// 使用CRON表达式,精确定义“每小时的第5分钟”执行
|
||||
// --- 注册静态定时任务 ---
|
||||
_, err := s.gocronScheduler.Cron("5 * * * *").Tag("stats-aggregation").Do(func() {
|
||||
s.logger.Info("Executing hourly request stats aggregation...")
|
||||
// 为后台定时任务创建一个新的、空的 context
|
||||
ctx := context.Background()
|
||||
if err := s.statsService.AggregateHourlyStats(ctx); err != nil {
|
||||
s.logger.WithError(err).Error("Hourly stats aggregation failed.")
|
||||
@@ -47,17 +71,9 @@ func (s *Scheduler) Start() {
|
||||
if err != nil {
|
||||
s.logger.Errorf("Failed to schedule [stats-aggregation]: %v", err)
|
||||
}
|
||||
|
||||
// 任务二:(预留) 自动健康检查
|
||||
|
||||
// 任务三:每日执行一次的软删除Key清理
|
||||
// Executes once daily at 3:15 AM UTC.
|
||||
_, err = s.gocronScheduler.Cron("15 3 * * *").Tag("cleanup-soft-deleted-keys").Do(func() {
|
||||
s.logger.Info("Executing daily cleanup of soft-deleted API keys...")
|
||||
|
||||
// [假设保留7天,实际应来自配置
|
||||
const retentionDays = 7
|
||||
|
||||
count, err := s.keyRepo.HardDeleteSoftDeletedBefore(time.Now().AddDate(0, 0, -retentionDays))
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Daily cleanup of soft-deleted keys failed.")
|
||||
@@ -70,13 +86,125 @@ func (s *Scheduler) Start() {
|
||||
if err != nil {
|
||||
s.logger.Errorf("Failed to schedule [cleanup-soft-deleted-keys]: %v", err)
|
||||
}
|
||||
|
||||
// --- 动态任务初始化 ---
|
||||
if err := s.UpdateLogCleanupTask(); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to initialize log cleanup task on startup.")
|
||||
}
|
||||
// --- 启动后台监听器和调度器 ---
|
||||
s.wg.Add(1)
|
||||
go s.listenForSettingsUpdates()
|
||||
s.gocronScheduler.StartAsync()
|
||||
s.logger.Info("Scheduler started.")
|
||||
}
|
||||
func (s *Scheduler) listenForSettingsUpdates() {
|
||||
defer s.wg.Done()
|
||||
s.logger.Info("Starting listener for system settings updates...")
|
||||
for {
|
||||
select {
|
||||
case <-s.stopChan:
|
||||
s.logger.Info("Stopping settings update listener.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
subscription, err := s.store.Subscribe(ctx, settings.SettingsUpdateChannel)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Warnf("Failed to subscribe to settings channel, retrying in 5s...")
|
||||
cancel()
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
s.logger.Infof("Successfully subscribed to channel '%s'.", settings.SettingsUpdateChannel)
|
||||
listenLoop:
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-subscription.Channel():
|
||||
if !ok {
|
||||
s.logger.Warn("Subscription channel closed by publisher. Re-subscribing...")
|
||||
break listenLoop
|
||||
}
|
||||
s.logger.Infof("Received settings update notification: %s", string(msg.Payload))
|
||||
if err := s.UpdateLogCleanupTask(); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to update log cleanup task after notification.")
|
||||
}
|
||||
case <-s.stopChan:
|
||||
s.logger.Info("Stopping settings update listener.")
|
||||
subscription.Close()
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
subscription.Close()
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// [MODIFIED] - UpdateLogCleanupTask 现在会动态生成 cron 表达式
|
||||
func (s *Scheduler) UpdateLogCleanupTask() error {
|
||||
if err := s.gocronScheduler.RemoveByTag(LogCleanupTaskTag); err != nil {
|
||||
// This is not an error, just means the job didn't exist
|
||||
}
|
||||
|
||||
settings := s.settingsManager.GetSettings()
|
||||
if !settings.LogAutoCleanupEnabled || settings.LogAutoCleanupRetentionDays <= 0 {
|
||||
s.logger.Info("Log auto-cleanup is disabled. Task removed or not scheduled.")
|
||||
return nil
|
||||
}
|
||||
|
||||
days := settings.LogAutoCleanupRetentionDays
|
||||
|
||||
// [NEW] 解析时间并生成 cron 表达式
|
||||
cronSpec, err := parseTimeToCron(settings.LogAutoCleanupTime)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Warnf("Invalid cleanup time format '%s'. Falling back to default '04:05'.", settings.LogAutoCleanupTime)
|
||||
cronSpec = "5 4 * * *" // 安全回退
|
||||
}
|
||||
|
||||
s.logger.Infof("Scheduling/updating daily log cleanup task to retain last %d days of logs, using cron spec: '%s'", days, cronSpec)
|
||||
|
||||
_, err = s.gocronScheduler.Cron(cronSpec).Tag(LogCleanupTaskTag).Do(func() {
|
||||
s.logger.Infof("Executing daily log cleanup, deleting logs older than %d days...", days)
|
||||
ctx := context.Background()
|
||||
deletedCount, err := s.logService.DeleteOldLogs(ctx, days)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Daily log cleanup task failed.")
|
||||
} else {
|
||||
s.logger.Infof("Daily log cleanup task completed. Deleted %d old logs.", deletedCount)
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to schedule new log cleanup task.")
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Log cleanup task updated successfully.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// [NEW] - 用于解析 "HH:mm" 格式时间为 cron 表达式的辅助函数
|
||||
func parseTimeToCron(timeStr string) (string, error) {
|
||||
parts := strings.Split(timeStr, ":")
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("invalid time format, expected HH:mm")
|
||||
}
|
||||
|
||||
hour, err := strconv.Atoi(parts[0])
|
||||
if err != nil || hour < 0 || hour > 23 {
|
||||
return "", fmt.Errorf("invalid hour value: %s", parts[0])
|
||||
}
|
||||
|
||||
minute, err := strconv.Atoi(parts[1])
|
||||
if err != nil || minute < 0 || minute > 59 {
|
||||
return "", fmt.Errorf("invalid minute value: %s", parts[1])
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d %d * * *", minute, hour), nil
|
||||
}
|
||||
func (s *Scheduler) Stop() {
|
||||
s.logger.Info("Stopping scheduler...")
|
||||
close(s.stopChan)
|
||||
s.gocronScheduler.Stop()
|
||||
s.logger.Info("Scheduler stopped.")
|
||||
s.wg.Wait()
|
||||
s.logger.Info("Scheduler stopped gracefully.")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user