Implement auto-cancellation of concurrent jobs if the event is push (#25716)
- cancel running jobs if the event is push - Add a new function `CancelRunningJobs` to cancel all running jobs of a run - Update `FindRunOptions` struct to include `Ref` field and update its condition in `toConds` function - Implement auto cancellation of running jobs in the same workflow in `notify` function related task: https://github.com/go-gitea/gitea/pull/22751/ --------- Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> Signed-off-by: appleboy <appleboy.tw@gmail.com> Co-authored-by: Jason Song <i@wolfogre.com> Co-authored-by: delvh <dev.lh@web.de>
This commit is contained in:
parent
5db640abcd
commit
44781f9f5c
6 changed files with 128 additions and 20 deletions
|
@ -34,7 +34,7 @@ type ActionRun struct {
|
||||||
Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository
|
Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository
|
||||||
TriggerUserID int64 `xorm:"index"`
|
TriggerUserID int64 `xorm:"index"`
|
||||||
TriggerUser *user_model.User `xorm:"-"`
|
TriggerUser *user_model.User `xorm:"-"`
|
||||||
Ref string
|
Ref string `xorm:"index"` // the commit/tag/… that caused the run
|
||||||
CommitSHA string
|
CommitSHA string
|
||||||
IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow.
|
IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow.
|
||||||
NeedApproval bool // may need approval if it's a fork pull request
|
NeedApproval bool // may need approval if it's a fork pull request
|
||||||
|
@ -164,6 +164,73 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow.
|
||||||
|
func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string) error {
|
||||||
|
// Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'.
|
||||||
|
runs, total, err := FindRuns(ctx, FindRunOptions{
|
||||||
|
RepoID: repoID,
|
||||||
|
Ref: ref,
|
||||||
|
WorkflowID: workflowID,
|
||||||
|
Status: []Status{StatusRunning, StatusWaiting},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no runs found, there's no need to proceed with cancellation, so return nil.
|
||||||
|
if total == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over each found run and cancel its associated jobs.
|
||||||
|
for _, run := range runs {
|
||||||
|
// Find all jobs associated with the current run.
|
||||||
|
jobs, _, err := FindRunJobs(ctx, FindRunJobOptions{
|
||||||
|
RunID: run.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over each job and attempt to cancel it.
|
||||||
|
for _, job := range jobs {
|
||||||
|
// Skip jobs that are already in a terminal state (completed, cancelled, etc.).
|
||||||
|
status := job.Status
|
||||||
|
if status.IsDone() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the job has no associated task (probably an error), set its status to 'Cancelled' and stop it.
|
||||||
|
if job.TaskID == 0 {
|
||||||
|
job.Status = StatusCancelled
|
||||||
|
job.Stopped = timeutil.TimeStampNow()
|
||||||
|
|
||||||
|
// Update the job's status and stopped time in the database.
|
||||||
|
n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again.
|
||||||
|
if n == 0 {
|
||||||
|
return fmt.Errorf("job has changed, try again")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with the next job.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the job has an associated task, try to stop the task, effectively cancelling the job.
|
||||||
|
if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return nil to indicate successful cancellation of all running and waiting jobs.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// InsertRun inserts a run
|
// InsertRun inserts a run
|
||||||
func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error {
|
func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error {
|
||||||
ctx, commiter, err := db.TxContext(ctx)
|
ctx, commiter, err := db.TxContext(ctx)
|
||||||
|
|
|
@ -68,10 +68,11 @@ type FindRunOptions struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
RepoID int64
|
RepoID int64
|
||||||
OwnerID int64
|
OwnerID int64
|
||||||
WorkflowFileName string
|
WorkflowID string
|
||||||
|
Ref string // the commit/tag/… that caused this workflow
|
||||||
TriggerUserID int64
|
TriggerUserID int64
|
||||||
Approved bool // not util.OptionalBool, it works only when it's true
|
Approved bool // not util.OptionalBool, it works only when it's true
|
||||||
Status Status
|
Status []Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts FindRunOptions) toConds() builder.Cond {
|
func (opts FindRunOptions) toConds() builder.Cond {
|
||||||
|
@ -82,8 +83,8 @@ func (opts FindRunOptions) toConds() builder.Cond {
|
||||||
if opts.OwnerID > 0 {
|
if opts.OwnerID > 0 {
|
||||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||||
}
|
}
|
||||||
if opts.WorkflowFileName != "" {
|
if opts.WorkflowID != "" {
|
||||||
cond = cond.And(builder.Eq{"workflow_id": opts.WorkflowFileName})
|
cond = cond.And(builder.Eq{"workflow_id": opts.WorkflowID})
|
||||||
}
|
}
|
||||||
if opts.TriggerUserID > 0 {
|
if opts.TriggerUserID > 0 {
|
||||||
cond = cond.And(builder.Eq{"trigger_user_id": opts.TriggerUserID})
|
cond = cond.And(builder.Eq{"trigger_user_id": opts.TriggerUserID})
|
||||||
|
@ -91,8 +92,11 @@ func (opts FindRunOptions) toConds() builder.Cond {
|
||||||
if opts.Approved {
|
if opts.Approved {
|
||||||
cond = cond.And(builder.Gt{"approved_by": 0})
|
cond = cond.And(builder.Gt{"approved_by": 0})
|
||||||
}
|
}
|
||||||
if opts.Status > StatusUnknown {
|
if len(opts.Status) > 0 {
|
||||||
cond = cond.And(builder.Eq{"status": opts.Status})
|
cond = cond.And(builder.In("status", opts.Status))
|
||||||
|
}
|
||||||
|
if opts.Ref != "" {
|
||||||
|
cond = cond.And(builder.Eq{"ref": opts.Ref})
|
||||||
}
|
}
|
||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
|
@ -517,6 +517,8 @@ var migrations = []Migration{
|
||||||
NewMigration("Reduce commit status", v1_21.ReduceCommitStatus),
|
NewMigration("Reduce commit status", v1_21.ReduceCommitStatus),
|
||||||
// v267 -> v268
|
// v267 -> v268
|
||||||
NewMigration("Add action_tasks_version table", v1_21.CreateActionTasksVersionTable),
|
NewMigration("Add action_tasks_version table", v1_21.CreateActionTasksVersionTable),
|
||||||
|
// v268 -> v269
|
||||||
|
NewMigration("Update Action Ref", v1_21.UpdateActionsRefIndex),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current db version
|
// GetCurrentDBVersion returns the current db version
|
||||||
|
|
16
models/migrations/v1_21/v268.go
Normal file
16
models/migrations/v1_21/v268.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_21 //nolint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateActionsRefIndex updates the index of actions ref field
|
||||||
|
func UpdateActionsRefIndex(x *xorm.Engine) error {
|
||||||
|
type ActionRun struct {
|
||||||
|
Ref string `xorm:"index"` // the commit/tag/… causing the run
|
||||||
|
}
|
||||||
|
return x.Sync(new(ActionRun))
|
||||||
|
}
|
|
@ -151,9 +151,13 @@ func List(ctx *context.Context) {
|
||||||
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
||||||
},
|
},
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
WorkflowFileName: workflow,
|
WorkflowID: workflow,
|
||||||
TriggerUserID: actorID,
|
TriggerUserID: actorID,
|
||||||
Status: actions_model.Status(status),
|
}
|
||||||
|
|
||||||
|
// if status is not StatusUnknown, it means user has selected a status filter
|
||||||
|
if actions_model.Status(status) != actions_model.StatusUnknown {
|
||||||
|
opts.Status = []actions_model.Status{actions_model.Status(status)}
|
||||||
}
|
}
|
||||||
|
|
||||||
runs, total, err := actions_model.FindRuns(ctx, opts)
|
runs, total, err := actions_model.FindRuns(ctx, opts)
|
||||||
|
|
|
@ -230,16 +230,31 @@ func notify(ctx context.Context, input *notifyInput) error {
|
||||||
log.Error("jobparser.Parse: %v", err)
|
log.Error("jobparser.Parse: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cancel running jobs if the event is push
|
||||||
|
if run.Event == webhook_module.HookEventPush {
|
||||||
|
// cancel running jobs of the same workflow
|
||||||
|
if err := actions_model.CancelRunningJobs(
|
||||||
|
ctx,
|
||||||
|
run.RepoID,
|
||||||
|
run.Ref,
|
||||||
|
run.WorkflowID,
|
||||||
|
); err != nil {
|
||||||
|
log.Error("CancelRunningJobs: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := actions_model.InsertRun(ctx, run, jobs); err != nil {
|
if err := actions_model.InsertRun(ctx, run, jobs); err != nil {
|
||||||
log.Error("InsertRun: %v", err)
|
log.Error("InsertRun: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: run.ID}); err != nil {
|
|
||||||
log.Error("FindRunJobs: %v", err)
|
|
||||||
} else {
|
|
||||||
CreateCommitStatus(ctx, jobs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
alljobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: run.ID})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("FindRunJobs: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
CreateCommitStatus(ctx, alljobs...)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue