fix(actions): run every due schedule exactly once per occurrence (#39078)

Signed-off-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
bircni
2026-08-29 06:44:51 +00:00
committed by GitHub
co-authored by silverwind Lunny Xiao wxiaoguang
parent fedf219e77
commit 3c0bfe9463
6 changed files with 132 additions and 182 deletions
+8 -14
View File
@@ -40,17 +40,8 @@ func init() {
db.RegisterModel(new(ActionSchedule))
}
// GetSchedulesMapByIDs returns the schedules by given id slice.
func GetSchedulesMapByIDs(ctx context.Context, ids []int64) (map[int64]*ActionSchedule, error) {
schedules := make(map[int64]*ActionSchedule, len(ids))
if len(ids) == 0 {
return schedules, nil
}
return schedules, db.GetEngine(ctx).In("id", ids).Find(&schedules)
}
// CreateScheduleTask creates new schedule task.
func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error {
// CreateScheduleTaskBySchedules creates new schedule task.
func CreateScheduleTaskBySchedules(ctx context.Context, rows []*ActionSchedule) error {
// Return early if there are no rows to insert
if len(rows) == 0 {
return nil
@@ -74,13 +65,16 @@ func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error {
ScheduleID: row.ID,
Spec: spec,
}
// Parse the spec and check for errors
schedule, err := specRow.Parse()
if err != nil {
continue // skip to the next spec if there's an error
continue
}
specRow.Next = timeutil.TimeStamp(schedule.Next(now).Unix())
next := schedule.Next(now)
if next.IsZero() {
continue // the spec parses but can never occur, like "0 0 30 2 *"
}
specRow.Next = timeutil.TimeStamp(next.Unix())
// Insert the new schedule spec row
if err = db.Insert(ctx, specRow); err != nil {
+1 -1
View File
@@ -26,7 +26,7 @@ type ActionScheduleSpec struct {
// Next time the job will run, or the zero time if Cron has not been
// started or this entry's schedule is unsatisfiable
Next timeutil.TimeStamp `xorm:"index"`
// Prev is the last time this job was run, or the zero time if never.
// Prev is the occurrence this spec was last processed for, or the zero time if never.
Prev timeutil.TimeStamp
Spec string
-97
View File
@@ -1,97 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"context"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/container"
"xorm.io/builder"
)
type SpecList []*ActionScheduleSpec
func (specs SpecList) GetScheduleIDs() []int64 {
return container.FilterSlice(specs, func(spec *ActionScheduleSpec) (int64, bool) {
return spec.ScheduleID, true
})
}
func (specs SpecList) LoadSchedules(ctx context.Context) error {
scheduleIDs := specs.GetScheduleIDs()
schedules, err := GetSchedulesMapByIDs(ctx, scheduleIDs)
if err != nil {
return err
}
for _, spec := range specs {
spec.Schedule = schedules[spec.ScheduleID]
}
repoIDs := specs.GetRepoIDs()
repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs)
if err != nil {
return err
}
for _, spec := range specs {
spec.Repo = repos[spec.RepoID]
}
return nil
}
func (specs SpecList) GetRepoIDs() []int64 {
return container.FilterSlice(specs, func(spec *ActionScheduleSpec) (int64, bool) {
return spec.RepoID, true
})
}
func (specs SpecList) LoadRepos(ctx context.Context) error {
repoIDs := specs.GetRepoIDs()
repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs)
if err != nil {
return err
}
for _, spec := range specs {
spec.Repo = repos[spec.RepoID]
}
return nil
}
type FindSpecOptions struct {
db.ListOptions
RepoID int64
Next int64
}
func (opts FindSpecOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
}
if opts.Next > 0 {
cond = cond.And(builder.Lte{"next": opts.Next})
}
return cond
}
func (opts FindSpecOptions) ToOrders() string {
return "`id` DESC"
}
func FindSpecs(ctx context.Context, opts FindSpecOptions) (SpecList, int64, error) {
specs, total, err := db.FindAndCount[ActionScheduleSpec](ctx, opts)
if err != nil {
return nil, 0, err
}
if err := SpecList(specs).LoadSchedules(ctx); err != nil {
return nil, 0, err
}
return specs, total, nil
}