GitRoot

Craft your forge, Build your project, Grow your community freely
  1// SPDX-FileCopyrightText: 2026 Romain Maneschi <romain@gitroot.dev>
  2//
  3// SPDX-License-Identifier: EUPL-1.2
  4
  5package scheduler
  6
  7import (
  8	"context"
  9	"slices"
 10	"sync"
 11	"time"
 12
 13	"github.com/adhocore/gronx"
 14	"github.com/samber/oops"
 15	"gitroot.dev/server/background"
 16	"gitroot.dev/server/logger"
 17	"gitroot.dev/server/repository"
 18)
 19
 20type Manager struct {
 21	logger            *logger.Logger
 22	conf              needConf
 23	repoManager       needRepoManager
 24	backgroundManager needBackgroundManager
 25	tasksLock         sync.RWMutex
 26	tasks             []*task
 27}
 28
 29type needConf interface {
 30}
 31
 32type needRepoManager interface {
 33	GetRepositories(ctx context.Context) ([]string, error)
 34	Open(ctx context.Context, repoName string) (*repository.GitRootRepository, error)
 35}
 36
 37type needBackgroundManager interface {
 38	Schedule(task background.ScheduleTaskInput)
 39}
 40
 41func NewManager(conf needConf, repoManager needRepoManager) *Manager {
 42	m := &Manager{
 43		logger:      logger.NewLogger(logger.SCHEDULER_MANAGER),
 44		conf:        conf,
 45		repoManager: repoManager,
 46	}
 47	return m
 48}
 49
 50func (m *Manager) SetBackgroundManager(backgroundManager needBackgroundManager) {
 51	m.backgroundManager = backgroundManager
 52}
 53
 54func (m *Manager) findSchedule(ctx context.Context) ([]*task, error) {
 55	repositories, err := m.repoManager.GetRepositories(ctx)
 56	if err != nil {
 57		return nil, oops.Wrapf(err, "can't get repositories")
 58	}
 59	allSchedule := []*task{}
 60	for _, r := range repositories {
 61		all, err := m.findScheduleInProject(ctx, r)
 62		if err != nil {
 63			m.logger.Error("can't find tasks in project", err, logger.NewLoggerPair("project", r))
 64		}
 65		allSchedule = append(allSchedule, all...)
 66	}
 67	return allSchedule, nil
 68}
 69
 70func (m *Manager) Start(ctx context.Context) {
 71	allSchedule, err := m.findSchedule(ctx)
 72	if err != nil {
 73		m.logger.Error("can't find schedule", err)
 74	}
 75	m.tasksLock.Lock()
 76	m.tasks = allSchedule
 77	m.tasksLock.Unlock()
 78
 79	ticker := time.NewTicker(1 * time.Second)
 80out:
 81	for {
 82		select {
 83		case <-ticker.C:
 84			m.tick(time.Now())
 85		case <-ctx.Done():
 86			m.logger.Debug("scheduler off")
 87			break out
 88		}
 89	}
 90}
 91
 92func (m *Manager) tick(now time.Time) {
 93	m.tasksLock.RLock()
 94	defer m.tasksLock.RUnlock()
 95
 96	for _, task := range m.tasks {
 97		if !now.Before(task.nextRun) {
 98			m.logger.Debug("before next tick", logger.NewLoggerPair("nextRun", task.nextRun), logger.NewLoggerPair("expr", task.cronExpr), logger.NewLoggerPair("repoName", task.repoName), logger.NewLoggerPair("plugin", task.pluginPurl))
 99			nextTime, err := gronx.NextTickAfter(task.cronExpr, task.nextRun, false)
100			if err != nil {
101				m.logger.Error("invalid cron expr in tick", err, logger.NewLoggerPair("expr", task.cronExpr), logger.NewLoggerPair("repoName", task.repoName), logger.NewLoggerPair("plugin", task.pluginPurl))
102				continue
103			}
104			task.nextRun = nextTime
105			m.logger.Debug("after next tick", logger.NewLoggerPair("nextRun", task.nextRun), logger.NewLoggerPair("expr", task.cronExpr), logger.NewLoggerPair("repoName", task.repoName), logger.NewLoggerPair("plugin", task.pluginPurl))
106
107			m.backgroundManager.Schedule(background.ScheduleTaskInput{
108				RepoName:   task.repoName,
109				PluginPurl: task.pluginPurl,
110				PluginRun:  task.pluginRun,
111			})
112		}
113	}
114}
115
116func (m *Manager) LoadChanges(ctx context.Context, repoName string) {
117	m.logger.Info("reload tasks", logger.NewLoggerPair("repo", repoName))
118	all, err := m.findScheduleInProject(ctx, repoName)
119	if err != nil {
120		m.logger.Error("can't find tasks in project", err, logger.NewLoggerPair("project", repoName))
121	}
122	m.logger.Info("find tasks", logger.NewLoggerPair("repo", repoName), logger.NewLoggerPair("nb", len(all)))
123	m.tasksLock.Lock()
124	defer m.tasksLock.Unlock()
125	m.tasks = slices.DeleteFunc(m.tasks, func(t *task) bool {
126		return t.repoName == repoName
127	})
128	m.tasks = append(m.tasks, all...)
129	m.logger.Info("finish LoadChanges", logger.NewLoggerPair("repo", repoName))
130}