package worker import ( "context" "fmt" "log" "time" "proto-gen/internal/config" "proto-gen/internal/database" "proto-gen/internal/k8s" "proto-gen/internal/models" ) type Scheduler struct { db *database.DB k8sClient k8s.K8sClient cfg *config.Config pollInterval time.Duration running map[string]bool } func NewScheduler(db *database.DB, k8sClient k8s.K8sClient, cfg *config.Config) *Scheduler { return &Scheduler{ db: db, k8sClient: k8sClient, cfg: cfg, pollInterval: 5 * time.Second, running: make(map[string]bool), } } func (s *Scheduler) Start(ctx context.Context) { log.Println("scheduler started") ticker := time.NewTicker(s.pollInterval) defer ticker.Stop() for { select { case <-ctx.Done(): log.Println("scheduler stopped") return case <-ticker.C: s.processPendingTasks() s.trackRunningJobs() } } } func (s *Scheduler) processPendingTasks() { pendingTasks, err := s.db.GetPendingTasks(10) if err != nil { log.Printf("error getting pending tasks: %v", err) return } for _, task := range pendingTasks { if len(s.running) >= s.cfg.Cluster.MaxConcurrent { log.Printf("max concurrent reached, skipping task %s", task.ID) return } hasRunning, err := s.db.HasRunningTask(task.TargetRepo) if err != nil { log.Printf("error checking running task: %v", err) continue } if hasRunning { log.Printf("task %s: target repo %s already has running task", task.ID, task.TargetRepo) continue } s.startTask(task) } } func (s *Scheduler) startTask(task *models.Task) { envVars := map[string]string{ "GIT_BASE_URL": s.cfg.Git.BaseURL, "GIT_TOKEN": s.cfg.Git.Token, "PROTO_REPO": task.ProtoRepo, "PROTO_BRANCH": task.ProtoBranch, "TARGET_REPO": task.TargetRepo, "OUT_DIR": "/out", } jobName := fmt.Sprintf("proto-gen-%s", task.ID[:8]) job, err := s.k8sClient.CreateJob(&k8s.JobSpec{ Name: jobName, Namespace: s.cfg.Cluster.Namespace, Image: task.ToolchainImage, EnvVars: envVars, TimeoutSecond: s.cfg.Cluster.JobTimeout, }) if err != nil { log.Printf("task %s: failed to create job: %v", task.ID, err) task.Status = models.StatusFailed task.Error = fmt.Sprintf("failed to create K8s job: %v", err) s.db.UpdateTask(task) return } task.Status = models.StatusRunning if err := s.db.UpdateTask(task); err != nil { log.Printf("task %s: failed to update status: %v", task.ID, err) return } s.running[task.ID] = true log.Printf("task %s: job %s created", task.ID, job.Name) } func (s *Scheduler) trackRunningJobs() { for taskID := range s.running { task, err := s.db.GetTask(taskID) if err != nil { delete(s.running, taskID) continue } jobName := fmt.Sprintf("proto-gen-%s", task.ID[:8]) job, err := s.k8sClient.GetJob(jobName, s.cfg.Cluster.Namespace) if err != nil { log.Printf("task %s: error getting job: %v", taskID, err) continue } switch job.Status { case k8s.JobSucceeded: task.Status = models.StatusSuccess task.Log = job.Logs s.db.UpdateTask(task) delete(s.running, taskID) log.Printf("task %s: completed successfully", taskID) case k8s.JobFailed: task.Status = models.StatusFailed task.Error = "K8s job failed" task.Log = job.Logs s.db.UpdateTask(task) delete(s.running, taskID) log.Printf("task %s: failed", taskID) } } }